summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Documentation/Makefile1
-rw-r--r--Documentation/access-control.txt51
-rw-r--r--Documentation/cmd-ban-commit.txt60
-rw-r--r--Documentation/cmd-cherry-pick.txt2
-rw-r--r--Documentation/cmd-create-account.txt4
-rw-r--r--Documentation/cmd-create-group.txt11
-rw-r--r--Documentation/cmd-create-project.txt19
-rw-r--r--Documentation/cmd-hook-commit-msg.txt8
-rw-r--r--Documentation/cmd-index.txt40
-rw-r--r--Documentation/cmd-ls-groups.txt41
-rw-r--r--Documentation/cmd-ls-projects.txt57
-rw-r--r--Documentation/cmd-plugin-enable.txt44
-rw-r--r--Documentation/cmd-plugin-install.txt71
-rw-r--r--Documentation/cmd-plugin-ls.txt44
-rw-r--r--Documentation/cmd-plugin-reload.txt48
-rw-r--r--Documentation/cmd-plugin-remove.txt45
-rw-r--r--Documentation/cmd-query.txt8
-rw-r--r--Documentation/cmd-receive-pack.txt5
-rw-r--r--Documentation/cmd-replicate.txt103
-rw-r--r--Documentation/cmd-review.txt19
-rw-r--r--Documentation/cmd-set-account.txt96
-rw-r--r--Documentation/cmd-set-project.txt111
-rw-r--r--Documentation/cmd-set-reviewers.txt6
-rw-r--r--Documentation/cmd-show-connections.txt2
-rw-r--r--Documentation/cmd-stream-events.txt35
-rw-r--r--Documentation/cmd-test-submit-rule.txt93
-rw-r--r--Documentation/config-contact.txt2
-rw-r--r--Documentation/config-gerrit.txt323
-rw-r--r--Documentation/config-gitweb.txt2
-rw-r--r--Documentation/config-headerfooter.txt2
-rw-r--r--Documentation/config-hooks.txt41
-rw-r--r--Documentation/config-mail.txt39
-rw-r--r--Documentation/config-replication.txt273
-rw-r--r--Documentation/config-reverseproxy.txt2
-rw-r--r--Documentation/config-sso.txt2
-rw-r--r--Documentation/dev-contributing.txt5
-rw-r--r--Documentation/dev-design.txt29
-rw-r--r--Documentation/dev-eclipse.txt27
-rw-r--r--Documentation/dev-plugins.txt420
-rw-r--r--Documentation/dev-release-subproject.txt99
-rw-r--r--Documentation/dev-release.txt279
-rw-r--r--Documentation/error-branch-not-found.txt4
-rw-r--r--Documentation/error-change-closed.txt17
-rw-r--r--Documentation/error-change-does-not-belong-to-project.txt2
-rw-r--r--Documentation/error-change-not-found.txt2
-rw-r--r--Documentation/error-invalid-author.txt (renamed from Documentation/error-you-are-not-author.txt)20
-rw-r--r--Documentation/error-invalid-committer.txt (renamed from Documentation/error-you-are-not-committer.txt)18
-rw-r--r--Documentation/error-messages.txt4
-rw-r--r--Documentation/error-no-changes-made.txt4
-rw-r--r--Documentation/error-no-new-changes.txt2
-rw-r--r--Documentation/error-non-fast-forward.txt8
-rw-r--r--Documentation/error-not-a-gerrit-administrator.txt2
-rw-r--r--Documentation/error-not-a-gerrit-project.txt2
-rw-r--r--Documentation/error-not-allowed-to-upload-merges.txt4
-rw-r--r--Documentation/error-permission-denied.txt2
-rw-r--r--Documentation/error-prohibited-by-gerrit.txt11
-rw-r--r--Documentation/error-push-fails-due-to-commit-message.txt2
-rw-r--r--Documentation/error-squash-commits-first.txt6
-rw-r--r--Documentation/i18n-readme.txt2
-rw-r--r--Documentation/index.txt12
-rw-r--r--Documentation/install-j2ee.txt2
-rw-r--r--Documentation/install-quick.txt67
-rw-r--r--Documentation/install.txt12
-rw-r--r--Documentation/intro-quick.txt6
-rw-r--r--Documentation/json.txt84
-rw-r--r--Documentation/licenses.txt5
-rw-r--r--Documentation/pgm-ExportReviewNotes.txt4
-rw-r--r--Documentation/pgm-init.txt7
-rw-r--r--Documentation/prolog-change-facts.txt100
-rw-r--r--Documentation/prolog-cookbook.txt750
-rw-r--r--Documentation/refs-notes-review.txt111
-rw-r--r--Documentation/rest-api.txt393
-rw-r--r--Documentation/user-changeid.txt12
-rw-r--r--Documentation/user-custom-dashboards.txt48
-rw-r--r--Documentation/user-notify.txt127
-rw-r--r--Documentation/user-search.txt46
-rw-r--r--Documentation/user-submodules.txt16
-rw-r--r--Documentation/user-upload.txt8
-rw-r--r--ReleaseNotes/ReleaseNotes-2.2.2.txt4
-rw-r--r--ReleaseNotes/ReleaseNotes-2.5.1.txt94
-rw-r--r--ReleaseNotes/ReleaseNotes-2.5.2.txt138
-rw-r--r--ReleaseNotes/ReleaseNotes-2.5.3.txt22
-rw-r--r--ReleaseNotes/ReleaseNotes-2.5.4.txt22
-rw-r--r--ReleaseNotes/ReleaseNotes-2.5.txt1950
-rw-r--r--ReleaseNotes/index.txt53
-rw-r--r--SUBMITTING_PATCHES4
-rw-r--r--contrib/git-exproll.sh566
-rw-r--r--gerrit-antlr/.gitignore3
-rw-r--r--gerrit-antlr/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-antlr/pom.xml2
-rw-r--r--gerrit-cache-h2/.gitignore6
-rw-r--r--gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs4
-rw-r--r--gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs (renamed from gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs)0
-rw-r--r--gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs (renamed from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs)2
-rw-r--r--gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs (renamed from gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs)0
-rw-r--r--gerrit-cache-h2/pom.xml (renamed from gerrit-ehcache/pom.xml)25
-rw-r--r--gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java135
-rw-r--r--gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java198
-rw-r--r--gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java726
-rw-r--r--gerrit-common/.gitignore3
-rw-r--r--gerrit-common/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-common/pom.xml2
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java44
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java36
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java2
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java73
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java47
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java19
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java6
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java19
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java5
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java21
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java8
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java111
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java49
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java50
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java22
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java48
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java60
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java4
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java10
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java4
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java3
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java52
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java22
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java29
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java15
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java9
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java15
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java43
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java16
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java8
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java35
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java10
-rw-r--r--gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java1
-rw-r--r--gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java271
-rw-r--r--gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java114
-rw-r--r--gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java81
-rw-r--r--gerrit-extension-api/.gitignore6
-rw-r--r--gerrit-extension-api/.settings/org.eclipse.core.resources.prefs4
-rw-r--r--gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs (renamed from gerrit-ehcache/.settings/org.eclipse.core.resources.prefs)2
-rw-r--r--gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs269
-rw-r--r--gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs61
-rw-r--r--gerrit-extension-api/pom.xml73
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java52
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java52
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java26
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java41
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java39
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java42
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java42
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java)15
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java34
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java (renamed from gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java)5
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java29
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java163
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java48
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java253
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java65
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java97
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java175
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java21
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java24
-rw-r--r--gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java42
-rw-r--r--gerrit-gwtdebug/.gitignore3
-rw-r--r--gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-gwtdebug/pom.xml2
-rw-r--r--gerrit-gwtui/.gitignore3
-rw-r--r--gerrit-gwtui/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-gwtui/pom.xml23
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java34
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java37
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java99
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java16
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties17
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java3
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java24
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java140
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java36
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java35
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java56
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java7
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java28
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java30
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java186
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java30
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java30
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java140
-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.java13
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties15
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java121
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java95
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java92
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java5
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java20
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java11
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java18
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java107
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java31
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java117
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml14
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java58
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java56
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java7
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java68
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java129
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java52
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java8
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java9
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java1
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java123
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java30
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java86
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java72
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java85
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java416
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java94
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml102
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java112
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java41
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java240
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java6
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java139
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java13
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java22
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java139
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java216
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java40
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.pngbin0 -> 911 bytes
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css79
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java16
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java7
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties13
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties5
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties4
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java142
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java5
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml33
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java189
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml98
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java71
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml65
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml42
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java241
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java28
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java30
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java46
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java58
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java7
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java55
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java93
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java57
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java215
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java38
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java10
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java (renamed from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java)33
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java2
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java160
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java38
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java36
-rw-r--r--gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java13
-rw-r--r--gerrit-httpd/.gitignore3
-rw-r--r--gerrit-httpd/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-httpd/pom.xml2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java86
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java43
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java20
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java24
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java4
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java47
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java41
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java29
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java (renamed from gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java)8
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java202
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java35
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java83
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java (renamed from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java)27
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java232
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java58
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java97
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java263
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java39
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java27
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java11
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java55
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java4
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java11
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java69
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java53
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java596
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java40
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java45
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java24
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java66
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java178
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java52
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java34
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java322
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java153
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java99
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java33
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java189
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java75
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java82
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java98
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java (renamed from gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java)6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java86
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java20
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java94
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java23
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java63
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java16
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java20
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java6
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java7
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java7
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java25
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java68
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java10
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java158
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java10
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java4
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java24
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java70
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java26
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java190
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java43
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java2
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java147
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java40
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java3
-rw-r--r--gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java75
-rw-r--r--gerrit-launcher/.gitignore3
-rw-r--r--gerrit-launcher/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-launcher/pom.xml2
-rw-r--r--gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java103
-rw-r--r--gerrit-main/.gitignore3
-rw-r--r--gerrit-main/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-main/pom.xml2
-rw-r--r--gerrit-openid/.gitignore3
-rw-r--r--gerrit-openid/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-openid/pom.xml5
-rw-r--r--gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java5
-rw-r--r--gerrit-package-plugins/.gitignore6
-rw-r--r--gerrit-package-plugins/pom.xml90
-rw-r--r--gerrit-patch-commonsnet/.gitignore3
-rw-r--r--gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-patch-commonsnet/pom.xml2
-rw-r--r--gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java21
-rw-r--r--gerrit-patch-jgit/.gitignore3
-rw-r--r--gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-patch-jgit/pom.xml2
-rw-r--r--gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java2
-rw-r--r--gerrit-pgm/.gitignore3
-rw-r--r--gerrit-pgm/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-pgm/pom.xml2
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java114
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java26
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java1
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java13
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java2
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java3
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java28
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java12
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java4
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java4
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java1
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java140
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java11
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java12
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java2
-rw-r--r--gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java2
-rwxr-xr-xgerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh5
-rw-r--r--gerrit-plugin-api/.gitignore8
-rw-r--r--gerrit-plugin-api/pom.xml110
-rw-r--r--gerrit-plugin-archetype/.gitignore (renamed from gerrit-ehcache/.gitignore)2
-rw-r--r--gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs4
-rw-r--r--gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs269
-rw-r--r--gerrit-plugin-archetype/pom.xml53
-rw-r--r--gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml65
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore5
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE201
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml103
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java)14
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java)13
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java24
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md1
-rw-r--r--gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md1
-rw-r--r--gerrit-prettify/.gitignore3
-rw-r--r--gerrit-prettify/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-prettify/pom.xml2
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java1
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties3
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java17
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java2
-rw-r--r--gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css14
-rw-r--r--gerrit-reviewdb/.gitignore3
-rw-r--r--gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-reviewdb/pom.xml2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java59
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java122
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java13
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java26
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java83
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java118
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java7
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java8
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java3
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java3
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java4
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java140
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java5
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java14
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java34
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java4
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java35
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java3
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java3
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java33
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java9
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java2
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java1
-rw-r--r--gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java4
-rw-r--r--gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql25
-rw-r--r--gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql25
-rw-r--r--gerrit-server/.gitignore3
-rw-r--r--gerrit-server/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-server/pom.xml18
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java172
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java24
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java28
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java35
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java68
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java18
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java81
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java46
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java95
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java299
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java62
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java75
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java)36
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java71
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java53
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java54
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java63
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java99
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java43
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java50
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java17
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java30
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java19
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java51
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java89
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java284
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java39
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java55
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java)9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java94
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java8
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java13
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java161
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java55
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java)16
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java78
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java47
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java)4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java58
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java227
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java38
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java151
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java35
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java46
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java181
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java190
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java)15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java48
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java40
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java60
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java35
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java62
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java42
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java138
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java39
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java96
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java)26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java45
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java14
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java25
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java18
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java8
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java145
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java)2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java22
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java108
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java17
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java22
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java73
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java)31
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java156
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java54
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java47
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java223
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java266
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java271
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java104
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java100
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java294
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java75
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java433
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java668
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java1351
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java4
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java8
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java59
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java1
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java92
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java47
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java59
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java15
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java215
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java19
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java82
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java13
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java205
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java58
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java59
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java33
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java60
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java25
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java3
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java13
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java21
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java (renamed from gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java)29
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java29
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java5
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java62
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java16
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java34
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java30
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java29
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java393
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java47
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java102
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java27
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java116
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java325
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java100
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java494
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java23
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java512
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java33
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java52
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java20
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java28
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java20
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java105
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java116
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java222
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java117
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java174
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java42
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java17
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java51
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java117
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java164
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java13
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java672
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java104
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java6
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java22
-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/SchemaVersionCheck.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java12
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java9
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java11
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java461
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java46
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java93
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java60
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java228
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java61
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java43
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java25
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java43
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java2
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java40
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java10
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java26
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java64
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java87
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java19
-rw-r--r--gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java3
-rw-r--r--gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java1
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java16
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java1
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_change_project_1.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java5
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java2
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_current_user_1.java4
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_current_user_2.java77
-rw-r--r--gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java2
-rw-r--r--gerrit-server/src/main/prolog/gerrit_common.pl20
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css39
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm3
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm6
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm9
-rw-r--r--gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg2
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java54
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java2
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java57
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java44
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java10
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java138
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java25
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java15
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java2
-rw-r--r--gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java6
-rw-r--r--gerrit-sshd/.gitignore3
-rw-r--r--gerrit-sshd/.settings/org.eclipse.core.resources.prefs3
-rw-r--r--gerrit-sshd/pom.xml9
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java7
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java125
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java (renamed from gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java)32
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java139
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java16
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java7
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java16
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java75
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java57
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java10
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java49
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java74
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java45
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java2
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java47
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java39
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java60
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java89
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java73
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java59
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java25
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java34
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java33
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java101
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java36
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java34
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java41
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java94
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java11
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java57
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java38
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java57
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java12
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java4
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java48
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java103
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java42
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java51
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java42
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java27
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java19
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java37
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java97
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java162
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java290
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java171
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java40
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java253
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java45
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java37
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java3
-rw-r--r--[-rwxr-xr-x]gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java0
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java241
-rw-r--r--gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java29
-rw-r--r--gerrit-util-cli/.gitignore3
-rw-r--r--gerrit-util-cli/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-util-cli/pom.xml2
-rw-r--r--gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java75
-rw-r--r--gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java1
-rw-r--r--gerrit-util-ssl/.gitignore3
-rw-r--r--gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-util-ssl/pom.xml2
-rw-r--r--gerrit-war/.gitignore3
-rw-r--r--gerrit-war/.settings/org.eclipse.core.resources.prefs1
-rw-r--r--gerrit-war/pom.xml2
-rw-r--r--gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java2
-rw-r--r--gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java21
-rw-r--r--gerrit-war/src/main/resources/log4j.properties4
-rw-r--r--gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml2
-rw-r--r--gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml2
-rw-r--r--pom.xml92
-rwxr-xr-xtools/deploy_api.sh60
-rw-r--r--tools/gwtui_dbg.launch6
-rw-r--r--tools/pgm_daemon.launch4
-rwxr-xr-xtools/release.sh2
-rwxr-xr-xtools/version.sh2
797 files changed, 34707 insertions, 10376 deletions
diff --git a/.gitignore b/.gitignore
index f318b65be0..465893d783 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
/.settings/org.eclipse.jdt.core.prefs
/.settings/org.maven.ide.eclipse.prefs
/test_site
+/.idea
+/gerrit-parent.iml
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 5522239834..4c64dfec55 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -76,6 +76,7 @@ $(DOC_HTML): %.html : %.txt
@$(ASCIIDOC) -a toc \
-a data-uri \
-a 'revision=$(REVISION)' \
+ -a 'newline=\n' \
-b xhtml11 \
-f asciidoc.conf \
$(ASCIIDOC_EXTRA) \
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 8d89fa2fe2..879d1ac283 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -23,7 +23,7 @@ This is the Gerrit "root" identity.
Users in the 'Administrators' group can perform any action under
the Admin menu, to any group or project, without further validation
-of any other access controls. In most installations only those
+or any other access controls. In most installations only those
users who have direct filesystem and database access would be
placed into this group.
@@ -444,6 +444,17 @@ have false-negatives that shouldn't block the change.
A restart is required after making database changes.
See <<restart_changes,below>>.
+[[category_abandon]]
+Abandon
+~~~~~~~
+
+This category controls whether users are allowed to abandon changes
+to projects in Gerrit. It can give permission to abandon a specific
+change to a given ref.
+
+This also grants the permission to restore a change if the change
+can be uploaded.
+
[[category_create]]
Create reference
~~~~~~~~~~~~~~~~
@@ -451,7 +462,7 @@ Create reference
The create reference category controls whether it is possible to
create new references, branches or tags. This implies that the
reference must not already exist, it's not a destructive permission
-in that you can't overwrite or remove any previosuly existing
+in that you can't overwrite or remove any previously existing
references (and also discard any commits in the process).
It's probably most common to either permit the creation of a single
@@ -462,7 +473,7 @@ This permission is often given in conjunction with regular push
branch permissions, allowing the holder of both to create new branches
as well as bypass review for new commits on that branch.
-To push lightweight (non annotated) tags, grant
+To push lightweight (non-annotated) tags, grant
`Create Reference` for reference name `refs/tags/*`, as lightweight
tags are implemented just like branches in Git.
@@ -622,13 +633,19 @@ The force option has no function when granted to a branch in the
Push Merge Commits
~~~~~~~~~~~~~~~~~~~~
-The `Push Merge Commit` permits the user to upload merge commits.
-It's an addon to the <<category_push,Push>> access right, and so it
-won't be sufficient with only `Push Merge Commit` granted for a push
-to happen. Some projects wish to restrict merges to being created by
-Gerrit. By granting `Push` without `Push Merge Commit`, the only
+The `Push Merge Commit` access right permits the user to upload merge
+commits. It's an addon to the <<category_push,Push>> access right, and
+so it won't be sufficient with only `Push Merge Commit` granted for a
+push to happen. Some projects wish to restrict merges to being created
+by Gerrit. By granting `Push` without `Push Merge Commit`, the only
merges that enter the system will be those created by Gerrit.
+The reference name connected to a `Push Merge Commit` entry must always
+be prefixed with `refs/for/`, for example `refs/for/refs/heads/BRANCH`.
+This applies even for an entry that complements a `Push` entry for
+`refs/heads/BRANCH` that allows direct pushes of non-merge commits, and
+the intention of the `Push Merge Commit` entry is to allow direct pushes
+of merge commits.
[[category_push_annotated]]
Push Annotated Tag
@@ -809,7 +826,7 @@ Examples of typical roles in a project
Below follows a set of typical roles on a server and which access
rights these roles typically should be granted. You may see them as
-general guide lines for a typical way to set up your project on a
+general guidelines for a typical way to set up your project on a
brand new Gerrit instance.
[[examples_contributor]]
@@ -892,8 +909,8 @@ submit of the change even if someone else sets `Verify` +1. Depending on the
project and how much the CI system can be trusted for accurate results, a
blocking label might not be feasible. A recommended alternative is to set the
label `Code-review` to -1 instead, as it isn't a blocking label but still
-shows a red label in the Gerrit UI. Optionally; to enable the possibility to
-deliver different results (build error vs unstable for instance) it's also
+shows a red label in the Gerrit UI. Optionally, to enable the possibility to
+deliver different results (build error vs unstable for instance), it's also
possible to set `Code-review` +1 as well.
If pushing new changes is granted, it's possible to automate cherry-pick of
@@ -1064,6 +1081,15 @@ Allow project creation. This capability allows the granted group to
either link:cmd-create-project.html[create new git projects via ssh]
or via the web UI.
+[[capability_emailReviewers]]
+Email Reviewers
+~~~~~~~~~~~~~~~
+
+Allow or deny sending email to change reviewers and watchers. This can be used
+to deny build bots from emailing reviewers and people who watch the change.
+Instead, only the authors of the change and those who starred it will be
+emailed. The allow rules are evaluated before deny rules, however the default
+is to allow emailing, if no explicit rule is matched.
[[capability_flushCaches]]
Flush Caches
@@ -1136,7 +1162,8 @@ command, but also to the web UI results pagination size.
Start Replication
~~~~~~~~~~~~~~~~~
-Allow access to execute link:cmd-replicate.html[the `gerrit replicate` command].
+Allow access to execute `replication start` command, if the
+replication plugin is installed on the server.
[[capability_viewCaches]]
diff --git a/Documentation/cmd-ban-commit.txt b/Documentation/cmd-ban-commit.txt
new file mode 100644
index 0000000000..fb4a2ac97c
--- /dev/null
+++ b/Documentation/cmd-ban-commit.txt
@@ -0,0 +1,60 @@
+gerrit ban-commit
+=================
+
+NAME
+----
+gerrit ban-commit - Bans a commit from a project's repository.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ban-commit'
+ [--reason <REASON>]
+ <PROJECT>
+ <COMMIT> ...
+
+DESCRIPTION
+-----------
+Marks a commit as banned for the specified repository. If a commit is
+banned Gerrit rejects every push that includes this commit with
+link:error-contains-banned-commit.html[contains banned commit ...].
+
+[NOTE]
+This command just marks the commit as banned, but it does not remove
+the commit from the history of any central branch. This needs to be
+done manually.
+
+ACCESS
+------
+Caller must be owner of the project or be a member of the privileged
+'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<PROJECT>::
+ Required; name of the project for which the commit should be
+ banned.
+
+<COMMIT>::
+ Required; commit(s) that should be banned.
+
+--reason::
+ Reason for banning the commit.
+
+EXAMPLES
+--------
+Ban commit `421919d015c062fd28901fe144a78a555d0b5984` from project
+`myproject`:
+
+====
+ $ ssh -p 29418 review.example.com gerrit ban-commit myproject \
+ 421919d015c062fd28901fe144a78a555d0b5984
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index 568c8729d6..d051a9a3aa 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -39,7 +39,7 @@ copy it to your local system:
====
$ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
- $ curl http://review.example.com/tools/bin/gerrit-cherry-pick
+ $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
====
GERRIT
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 98f950f048..16b2eb528e 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -13,6 +13,7 @@ SYNOPSIS
[--full-name <FULLNAME>]
[--email <EMAIL>]
[--ssh-key - | <KEY>]
+ [--http-password <PASSWORD>]
<USERNAME>
DESCRIPTION
@@ -59,6 +60,9 @@ This most likely requires double quoting the value, for example
--email::
Preferred email address for the user account.
+--http-password::
+ HTTP password for the user account.
+
EXAMPLES
--------
Create a new user account called `watcher`:
diff --git a/Documentation/cmd-create-group.txt b/Documentation/cmd-create-group.txt
index 475d2c5664..8dc6dccb6e 100644
--- a/Documentation/cmd-create-group.txt
+++ b/Documentation/cmd-create-group.txt
@@ -9,8 +9,8 @@ SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit create-group'
- [--owner <GROUP>]
- [--description <DESC>]
+ [--owner <GROUP> | -o <GROUP>]
+ [--description <DESC> | -d <DESC>]
[--member <USERNAME>]
[--group <GROUP>]
[--visible-to-all]
@@ -53,6 +53,13 @@ Description values containing spaces should be quoted in single quotes
--member::
User name to become initial member of the group. Multiple --member
options may be specified to add more initial members.
++
+Trying to add a user that doesn't have an account in Gerrit fails,
+unless LDAP is used for authentication. If LDAP is used for
+authentication and the user is not found, Gerrit tries to authenticate
+the user against the LDAP backend. If the authentication is successful
+a user account is automatically created, so that the user can be added
+to the group.
--group::
Group name to include in the group. Multiple --group options may
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index f22141c9a4..d0e56fd714 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -14,12 +14,12 @@ SYNOPSIS
[--suggest-parents | -S ]
[--permissions-only]
[--description <DESC> | -d <DESC>]
- [--submit-type <TYPE> | -t <TYPE>]
+ [--submit-type <TYPE> | -t <TYPE>]
[--use-contributor-agreements | --ca]
[--use-signed-off-by | --so]
[--use-content-merge]
[--require-change-id | --id]
- [--branch <REF> | -b <REF>]
+ [[--branch <REF> | -b <REF>] ...]
[--empty-commit]
{ <NAME> | --name <NAME> }
@@ -59,8 +59,11 @@ OPTIONS
--branch::
-b::
- Name of the initial branch in the newly created project.
- Defaults to 'master'.
+ Name of the initial branch(es) in the newly created project.
+ Several branches can be specified on the command line.
+ If several branches are specified then the first one becomes HEAD
+ of the project. If none branches are specified then default value
+ ('master') is used.
--owner::
-o::
@@ -163,7 +166,8 @@ the single quotes to delimit the value.
REPLICATION
-----------
-The remote repository creation is performed by a Bourne shell script:
+If the replication plugin is installed, the plugin will attempt to
+perform remote repository creation by a Bourne shell script:
====
mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master
@@ -174,10 +178,13 @@ arbitrary shell scripts, and must have `git` in the user's PATH
environment variable. Administrators could also run this command line
by hand to establish a new empty repository.
+A custom extension or plugin may also be developed to implement the
+NewProjectCreatedListener extension point and handle custom logic
+for remote repository creation.
+
SEE ALSO
--------
-* link:config-replication.html[Git Replication/Mirroring]
* link:project-setup.html[Project Setup]
GERRIT
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index 6318bba014..bd602c11cf 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -54,7 +54,7 @@ change viewed on the web.
OBTAINING
---------
To obtain the 'commit-msg' script use scp, wget or curl to download it
-to your local system from your gerrit server.
+to your local system from your Gerrit server.
You can use either of the below commands:
@@ -73,6 +73,12 @@ A specific example of this might look something like this:
$ curl -o ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
====
+Make sure the hook file is executable:
+
+====
+ $ chmod u+x ~/duhproject/.git/hooks/commit-msg
+====
+
SEE ALSO
--------
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index b09c3b363e..ccd9ffc191 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -12,8 +12,8 @@ To download a client command or hook, use scp or an http client:
$ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
$ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
- $ curl http://review.example.com/tools/bin/gerrit-cherry-pick
- $ curl http://review.example.com/tools/hooks/commit-msg
+ $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+ $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
For more details on how to determine the correct SSH port number,
see link:user-upload.html#test_ssh[Testing Your SSH Connection].
@@ -54,6 +54,9 @@ see link:user-upload.html#test_ssh[Testing Your SSH Connection].
'gerrit approve'::
'Deprecated alias for `gerrit review`.'
+link:cmd-ban-commit.html[gerrit ban-commit]::
+ Bans a commit from a project's repository.
+
link:cmd-ls-groups.html[gerrit ls-groups]::
List groups visible to the caller.
@@ -93,21 +96,24 @@ review. See link:user-upload.html#push_create[Creating Changes].
link:cmd-create-account.html[gerrit create-account]::
Create a new batch/role account.
+link:cmd-set-account.html[gerrit set-account]::
+ Change an account's settings.
+
link:cmd-create-group.html[gerrit create-group]::
Create a new account group.
link:cmd-create-project.html[gerrit create-project]::
Create a new project and associated Git repository.
+link:cmd-set-project.html[gerrit set-project]::
+ Change a project's settings.
+
link:cmd-flush-caches.html[gerrit flush-caches]::
Flush some/all server caches from memory.
link:cmd-gsql.html[gerrit gsql]::
Administrative interface to active database.
-link:cmd-replicate.html[gerrit replicate]::
- Manually trigger replication, to recover a node.
-
link:cmd-set-project-parent.html[gerrit set-project-parent]::
Change the project permissions are inherited from.
@@ -120,6 +126,30 @@ link:cmd-show-connections.html[gerrit show-connections]::
link:cmd-show-queue.html[gerrit show-queue]::
Display the background work queues, including replication.
+link:cmd-plugin-install.html[gerrit plugin add]::
+ Alias for 'gerrit plugin install'.
+
+link:cmd-plugin-enable.html[gerrit plugin enable]::
+ Enable plugins.
+
+link:cmd-plugin-install.html[gerrit plugin install]::
+ Install/Add a plugin.
+
+link:cmd-plugin-ls.html[gerrit plugin ls]::
+ List the installed plugins.
+
+link:cmd-plugin-reload.html[gerrit plugin reload]::
+ Reload/Restart plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin remove]::
+ Disable plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin rm]::
+ Alias for 'gerrit plugin remove'.
+
+link:cmd-test-submit-rule.html[gerrit test-submit-rule]::
+ Test prolog submit rules.
+
link:cmd-kill.html[kill]::
Kills a scheduled or running task.
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 8564db258e..306bb92e18 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -9,10 +9,11 @@ SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit ls-groups'
- [--project <NAME>]
- [--user <NAME>]
+ [--project <NAME> | -p <NAME>]
+ [--user <NAME> | -u <NAME>]
[--visible-to-all]
- [--type {internal | ldap | system}]
+ [--type {internal | system}]
+ [--verbose | -v]
DESCRIPTION
-----------
@@ -30,6 +31,12 @@ SCRIPTING
---------
This command is intended to be used in scripts.
+All non-printable characters (ASCII value 31 or less) are escaped
+according to the conventions used in languages like C, Python, and Perl,
+employing standard sequences like `\n` and `\t`, and `\xNN` for all
+others. In shell scripts, the `printf` command can be used to unescape
+the output.
+
OPTIONS
-------
--project::
@@ -65,10 +72,19 @@ This option can't be used together with the '--project' option.
+
--
`internal`:: Any group defined within Gerrit.
-`ldap`:: Any group defined by an external LDAP database.
`system`:: Any system defined and managed group.
--
+--verbose::
+-v::
+ Enable verbose output with tab-separated columns for the
+ group name, UUID, description, type (`SYSTEM` or `INTERNAL`),
+ owner group name, owner group UUID and whether the group is
+ visible to all (`true` or `false`).
++
+If a group has been "orphaned", i.e. its owner group UUID refers to a
+nonexistent group, the owner group name field will read `n/a`.
+
EXAMPLES
--------
@@ -91,6 +107,23 @@ List all groups for which any permission is set for the project
Registered Users
=====
+Extract the UUID of the 'Administrators' group:
+
+=====
+ $ ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $2}'
+ ad463411db3eec4e1efb0d73f55183c1db2fd82a
+=====
+
+Extract and expand the multi-line description of the 'Administrators'
+group:
+
+=====
+ $ printf "$(ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $3}')\n"
+ This is a
+ multi-line
+ description.
+=====
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 7782aa848b..d7d5aa59d1 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -10,8 +10,12 @@ SYNOPSIS
[verse]
'ssh' -p <port> <host> 'gerrit ls-projects'
[--show-branch <BRANCH> ...]
- [--tree]
+ [--description | -d]
+ [--tree | -t]
[--type {code | permissions | all}]
+ [--format {text | json | json_compact}]
+ [--all]
+ [--limit <N>]
DESCRIPTION
-----------
@@ -23,7 +27,7 @@ group, all projects are listed.
ACCESS
------
-Any user who has configured an SSH key.
+Any user who has configured an SSH key, or by an user over HTTP.
SCRIPTING
---------
@@ -42,12 +46,15 @@ OPTIONS
whole project is not shown.
--description::
---d::
+-d::
Allows listing of projects together with their respective
description.
+
-Line-feeds are escaped to allow ls-project to keep the
-"one project per line"-style.
+For text format output, all non-printable characters (ASCII value 31 or
+less) are escaped according to the conventions used in languages like C,
+Python, and Perl, employing standard sequences like `\n` and `\t`, and
+`\xNN` for all others. In shell scripts, the `printf` command can be
+used to unescape the output.
--tree::
-t::
@@ -64,6 +71,15 @@ Line-feeds are escaped to allow ls-project to keep the
`all`:: Any type of project.
--
+--format::
+ What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
--all::
Display all projects that are accessible by the calling user
account. Besides the projects that the calling user account has
@@ -72,12 +88,43 @@ Line-feeds are escaped to allow ls-project to keep the
the 'READ' access right is not assigned to the calling user
account).
+--limit::
+ Cap the number of results to the first N matches.
+
+HTTP
+----
+This command is also available over HTTP, as `/projects/` for
+anonymous access and `/a/projects/` for authenticated access.
+Named options are available as query parameters. Results can
+be limited to projects matching a prefix by supplying the prefix
+as part of the URL, for example `/projects/external/` lists only
+projects whose name start with the string `external/`.
+
+Over HTTP the `json_compact` output format is assumed if the client
+explicitly asks for JSON using HTTP header `Accept: application/json`.
+When any JSON output format is used on HTTP, readers must skip the
+first line produced. The first line is a garbage JSON string crafted
+to prevent a browser from executing the response in a script tag.
+
+Output will be gzip compressed if `Accept-Encoding: gzip` was used
+by the client in the request headers.
+
EXAMPLES
--------
List visible projects:
=====
$ ssh -p 29418 review.example.com gerrit ls-projects
+ platform/manifest
+ tools/gerrit
+ tools/gwtorm
+
+ $ curl http://review.example.com/projects/
+ platform/manifest
+ tools/gerrit
+ tools/gwtorm
+
+ $ curl http://review.example.com/projects/tools/
tools/gerrit
tools/gwtorm
=====
diff --git a/Documentation/cmd-plugin-enable.txt b/Documentation/cmd-plugin-enable.txt
new file mode 100644
index 0000000000..da651cab11
--- /dev/null
+++ b/Documentation/cmd-plugin-enable.txt
@@ -0,0 +1,44 @@
+plugin enable
+=============
+
+NAME
+----
+plugin enable - Enable plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin enable'
+ <NAME> ...
+
+DESCRIPTION
+-----------
+Enable plugins currently disabled. The plugins will be enabled by renaming
+the plugin jars in the site path's `plugins` directory from
+`<plugin-jar-name>.disabled` to `<plugin-jar-name>`.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+ Name of the plugin that should be enabled. Multiple names of
+ plugins that should be enabled may be specified.
+
+EXAMPLES
+--------
+Enable a plugin:
+
+====
+ ssh -p 29418 localhost gerrit plugin enable my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-install.txt b/Documentation/cmd-plugin-install.txt
new file mode 100644
index 0000000000..79d1f4a5a9
--- /dev/null
+++ b/Documentation/cmd-plugin-install.txt
@@ -0,0 +1,71 @@
+plugin install
+==============
+
+NAME
+----
+plugin install - Install/Add a plugin.
+
+plugin add - Install/Add a plugin.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin install | add'
+ [--name <NAME> | -n <NAME>]
+ - | <URL> | <PATH>
+
+DESCRIPTION
+-----------
+Install/Add a plugin. The plugin will be copied into the site path's
+`plugins` directory.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+-::
+ Plugin jar as piped input.
+
+<URL>::
+ URL from where the plugin should be downloaded. This can be an
+ HTTP or FTP site.
+
+<PATH>::
+ Absolute file path to the plugin jar.
+
+--name::
+-n::
+ The name under which the plugin should be installed.
+
+EXAMPLES
+--------
+Install a plugin from an absolute file path on the server's host:
+
+====
+ ssh -p 29418 localhost gerrit plugin install -n name \
+ $(pwd)/my-plugin.jar
+====
+
+Install a plugin from an HTTP site:
+
+====
+ ssh -p 29418 localhost gerrit plugin install -n name \
+ http://build-server/output/our-plugin.jar
+====
+
+Install a plugin from piped input:
+
+====
+ ssh -p 29418 localhost gerrit plugin install -n name \
+ - <target/name-0.1.jar
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-ls.txt b/Documentation/cmd-plugin-ls.txt
new file mode 100644
index 0000000000..6cce83cfc3
--- /dev/null
+++ b/Documentation/cmd-plugin-ls.txt
@@ -0,0 +1,44 @@
+plugin ls
+=========
+
+NAME
+----
+plugin ls - List the installed plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin ls'
+ [--all | -a]
+ [--format {text | json | json_compact}]
+
+DESCRIPTION
+-----------
+List the installed plugins and show their version and status.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+--all::
+-a::
+ List all plugins, including disabled plugins.
+
+--format::
+ What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-reload.txt b/Documentation/cmd-plugin-reload.txt
new file mode 100644
index 0000000000..3932e306ca
--- /dev/null
+++ b/Documentation/cmd-plugin-reload.txt
@@ -0,0 +1,48 @@
+plugin reload
+=============
+
+NAME
+----
+plugin reload - Reload/Restart plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin reload'
+ <NAME> ...
+
+DESCRIPTION
+-----------
+Reload/Restart plugins.
+
+Whether a plugin is reloaded or restarted is defined by the plugin's
+link:dev-plugins.html#reload_method[reload method].
+
+E.g. a plugin needs to be reloaded if its configuration is modified to
+make the new configuration data become active.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+ Name of the plugin that should be reloaded. Multiple names of
+ plugins that should be reloaded may be specified.
+
+EXAMPLES
+--------
+Reload a plugin:
+
+====
+ ssh -p 29418 localhost gerrit plugin reload my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-remove.txt b/Documentation/cmd-plugin-remove.txt
new file mode 100644
index 0000000000..ab8f95b75b
--- /dev/null
+++ b/Documentation/cmd-plugin-remove.txt
@@ -0,0 +1,45 @@
+plugin remove
+=============
+
+NAME
+----
+plugin remove - Disable plugins.
+
+plugin rm - Disable plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin remove | rm'
+ <NAME> ...
+
+DESCRIPTION
+-----------
+Disable plugins. The plugins will be disabled by renaming the plugin
+jars in the site path's `plugins` directory to `<plugin-jar-name>.disabled`.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+ Name of the plugin that should be disabled. Multiple names of
+ plugins that should be disabled may be specified.
+
+EXAMPLES
+--------
+Disable a plugin:
+
+====
+ ssh -p 29418 localhost gerrit plugin remove my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 253bed18cc..2feea114c8 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -16,6 +16,7 @@ SYNOPSIS
[--comments]
[--commit-message]
[--dependencies]
+ [--submit-records]
[--]
<query>
[limit:<n>]
@@ -80,6 +81,11 @@ OPTIONS
Show information about patch sets which depend on, or are needed by,
each patch set.
+--submit-records::
+ Show submit record information about the change, which
+ includes whether the change meets the criteria for submission
+ (including information for each review label).
+
limit:<n>::
Maximum number of results to return. This is actually a
query operator, and not a command line option. If more
@@ -124,7 +130,7 @@ SCHEMA
------
The JSON messages consist of nested objects referencing the
link:json.html#change[change],
-link:json.html#patchset[patchset],
+link:json.html#patchSet[patchset],
link:json.html#[account]
involved, and other attributes as appropriate.
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index 7e5ca09b2e..68f686d859 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -8,7 +8,10 @@ git-receive-pack - Receive what is pushed into the repository
SYNOPSIS
--------
[verse]
-'git receive-pack' [--reviewer <address>] [--cc <address>] <project>
+'git receive-pack'
+ [--reviewer <address> | --re <address>]
+ [--cc <address>]
+ <project>
DESCRIPTION
-----------
diff --git a/Documentation/cmd-replicate.txt b/Documentation/cmd-replicate.txt
deleted file mode 100644
index 7722027549..0000000000
--- a/Documentation/cmd-replicate.txt
+++ /dev/null
@@ -1,103 +0,0 @@
-gerrit replicate
-================
-
-NAME
-----
-gerrit replicate - Manually trigger replication, to recover a node
-
-SYNOPSIS
---------
-[verse]
-'ssh' -p <port> <host> 'gerrit replicate'
- [--url <PATTERN>]
- {--all | <PROJECT> ...}
-
-DESCRIPTION
------------
-Schedules replication of the specified projects to all configured
-replication destinations, or only those whose URLs match the pattern
-given on the command line.
-
-Normally Gerrit automatically schedules replication whenever it
-makes a change to a managed Git repository. However, there are
-other reasons why an administrator may wish to trigger replication:
-
-* Destination disappears, then later comes back online.
-+
-If a destination went offline for a period of time, when it comes
-back, it may be missing commits that it should have. Triggering a
-replication run for all projects against that URL will update it.
-
-* After repacking locally, and using `rsync` to distribute the new
- pack files to the destinations.
-+
-If the local server is repacked, and then the resulting pack files
-are sent to remote peers using `rsync -a --delete-after`, there
-is a chance that the rsync missed a change that was added during
-the rsync data transfer, and the rsync will remove that changes's
-data from the remote, even though the automatic replication pushed
-it there in parallel to the rsync.
-+
-Its a good idea to run replicate with `--all` to ensure all
-projects are consistent after the rsync is complete.
-
-* After deleting a ref by hand.
-+
-If a ref must be removed (e.g. to purge a change or patch set
-that shouldn't have been created, and that must be eradicated)
-that delete must be done by direct git access on the local,
-managed repository. Gerrit won't know about the delete, and is
-unable to replicate it automatically. Triggering replication on
-just the affected project can update the mirrors.
-
-ACCESS
-------
-Caller must be a member of the privileged 'Administrators' group,
-or have been granted
-link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
-
-SCRIPTING
----------
-This command is intended to be used in scripts.
-
-OPTIONS
--------
---all::
- Schedule replicating for all projects.
-
---url <PATTERN>::
- Replicate only to replication destinations whose URL
- contains the substring <PATTERN>. This can be useful to
- replicate only to a previously down node, which has been
- brought back online.
-
-EXAMPLES
---------
-Replicate every project, to every configured remote:
-
-====
- $ ssh -p 29418 review.example.com gerrit replicate --all
-====
-
-Replicate only to `srv2` now that it is back online:
-
-====
- $ ssh -p 29418 review.example.com gerrit replicate --url srv2 --all
-====
-
-Replicate only the `tools/gerrit` project, after deleting a ref
-locally by hand:
-
-====
- $ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
- $ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
-====
-
-SEE ALSO
---------
-
-* link:config-replication.html[Git Replication/Mirroring]
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index ac613e5586..513bc6e1ba 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -9,10 +9,10 @@ SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit review'
- [--project <PROJECT>]
- [--message <MESSAGE>]
+ [--project <PROJECT> | -p <PROJECT>]
+ [--message <MESSAGE> | -m <MESSAGE>]
[--force-message]
- [--submit]
+ [--submit | -s]
[--abandon | --restore]
[--publish]
[--delete]
@@ -52,16 +52,19 @@ OPTIONS
--force-message::
Option which allows Gerrit to publish the --message, even
- when the labels could not be applied due to change being
- closed).
+ when the labels could not be applied due to the change being
+ closed.
+
Used by some scripts/CI-systems, where the results (or links
to the result) are posted as a message after completion of a
build (often together with a label-change, indicating the success
of the build).
+
-If the message is posted successfully, the cmd will return
+If the message is posted successfully, the command will return
successfully, even if the label could not be changed.
++
+This option will not force the message to be posted if the command
+fails because the user is not permitted to change the label.
--help::
-h::
@@ -69,11 +72,11 @@ successfully, even if the label could not be changed.
complete listing of supported approval categories and values.
--abandon::
- Abandon the specified patch set(s).
+ Abandon the specified change(s).
(option is mutually exclusive with --submit and --restore)
--restore::
- Restore the specified abandoned patch set(s).
+ Restore the specified abandoned change(s).
(option is mutually exclusive with --abandon)
--submit::
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
new file mode 100644
index 0000000000..f9855cdd98
--- /dev/null
+++ b/Documentation/cmd-set-account.txt
@@ -0,0 +1,96 @@
+gerrit set-account
+==================
+
+NAME
+----
+gerrit set-account - Change an account's settings.
+
+SYNOPSIS
+--------
+[verse]
+set-account [--full-name <FULLNAME>] [--active|--inactive] \
+ [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
+ [--add-ssh-key - | <KEY>] \
+ [--delete-ssh-key - | <KEY> | ALL] \
+ [--http-password <PASSWORD>] <USER>
+
+DESCRIPTION
+-----------
+Modifies a given user's settings. This command can be useful to
+deactivate an account, set HTTP password, add/delete ssh keys without
+going through the UI.
+
+It also allows managing email addresses, which bypasses the
+verification step we force within the UI.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<USER>::
+ Required; Full name, email-address, SSH username or account id.
+
+--full-name::
+ Display name of the user account.
++
+Names containing spaces should be quoted in single quotes (').
+This most likely requires double quoting the value, for example
+`--full-name "'A description string'"`.
+
+--active::
+ Set the account state to be active.
+
+--inactive::
+ Set the account state to be inactive. This prevents the
+ user from logging in.
+
+--add-email::
+ Add another email to the user's account. This doesn't
+ trigger the mail validation and adds the email directly
+ to the user's account.
+ May be supplied more than once to add multiple emails to
+ an account in a single command execution.
+
+--delete-email::
+ Delete an email from this user's account if it exists.
+ If the email provided is 'ALL', all associated emails are
+ deleted from this account.
+ Maybe supplied more than once to remove multiple emails
+ from an account in a single command execution.
+
+--add-ssh-key::
+ Content of the public SSH key to add to the account's
+ keyring. If `-` the key is read from stdin, rather than
+ from the command line.
+ May be supplied more than once to add multiple SSH keys
+ in a single command execution.
+
+--delete-ssh-key::
+ Content of the public SSH key to remove from the account's
+ keyring or the comment associated with this key.
+ If `-` the key is read from stdin, rather than from the
+ command line. If the key provided is 'ALL', all
+ associated SSH keys are removed from this account.
+ May be supplied more than once to delete multiple SSH
+ keys in a single command execution.
+
+--http-password::
+ Set the HTTP password for the user account.
+
+EXAMPLES
+--------
+Add an email and SSH key to `watcher`'s account:
+
+====
+ $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit set-account --add-ssh-key - --add-email mail@example.com watcher
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
new file mode 100644
index 0000000000..059f063619
--- /dev/null
+++ b/Documentation/cmd-set-project.txt
@@ -0,0 +1,111 @@
+gerrit set-project
+==================
+
+NAME
+----
+gerrit set-project - Change a project's settings.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit set-project'
+ [--description <DESC> | -d <DESC>]
+ [--submit-type <TYPE> | -t <TYPE>]
+ [--use|no-contributor-agreements | --ca|nca]
+ [--use|no-signed-off-by | --so|nso]
+ [--use|no-content-merge]
+ [--require|no-change-id | --id|nid]
+ [--project-state | --ps]
+ <NAME>
+
+DESCRIPTION
+-----------
+Modifies a given project's settings. This command can be useful to
+batch change projects.
+
+The command is argument-safe, that is, if no argument is given the
+previous settings are kept intact.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+ Required; name of the project to edit. If name ends
+ with `.git` the suffix will be automatically removed.
+
+--description::
+-d::
+ New description of the project. If not specified,
+ the old description is kept.
++
+Description values containing spaces should be quoted in single quotes
+('). This most likely requires double quoting the value, for example
+`--description "'A description string'"`.
+
+--submit-type::
+-t::
+ Action used by Gerrit to submit an approved change to its
+ destination branch. Supported options are:
++
+* FAST_FORWARD_ONLY: produces a strictly linear history.
+* MERGE_IF_NECESSARY: create a merge commit when required.
+* MERGE_ALWAYS: always create a merge commit.
+* CHERRY_PICK: always cherry-pick the commit.
+
++
+For more details see
+link:project-setup.html#submit_type[Change Submit Actions].
+
+--use|no-content-merge::
+ If enabled, 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|no-contributor-agreements::
+--ca|nca::
+ If enabled, authors must complete a contributor agreement
+ on the site before pushing any commits or changes to this
+ project.
+
+--use|no-signed-off-by::
+--so|nso:
+ If enabled, each change must contain a Signed-off-by line
+ from either the author or the uploader in the commit message.
+
+--require|no-change-id::
+--id|nid::
+ Require a valid link:user-changeid.html[Change-Id] footer
+ in any commit uploaded for review. This does not apply to
+ commits pushed directly to a branch or tag.
+
+--project-state::
+--ps::
+ Set project's visibility.
++
+* ACTIVE: project is regular and is the default value.
+* READ_ONLY: users can see the project if read permission
+is granted, but all modification operations are disabled.
+* HIDDEN: the project is not visible for those who are not owners
+
+EXAMPLES
+--------
+Change project `example` to be hidden, require change id, don't use content merge
+and use 'merge if necessary' as merge strategy:
+
+====
+ $ ssh -p 29418 review.example.com gerrit set-project example --submit-type MERGE_IF_NECESSARY\
+ --require-change-id --no-content-merge --project-state HIDDEN
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review] \ No newline at end of file
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 9e08e39efa..32fd35e63a 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -9,9 +9,9 @@ SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit set-reviewers'
- [--project <PROJECT>]
- [--add REVIEWER ...]
- [--remove REVIEWER ...]
+ [--project <PROJECT> | -p <PROJECT>]
+ [--add <REVIEWER> ... | -a <REVIEWER> ...]
+ [--remove <REVIEWER> ... | -r <REVIEWER> ...]
[--]
{COMMIT | CHANGE-ID}...
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index b5d41bd7b6..8404a97c6d 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -8,7 +8,7 @@ gerrit show-connections - Display active client SSH connections
SYNOPSIS
--------
[verse]
-'ssh' -p <port> <host> 'gerrit show-connections' [-n]
+'ssh' -p <port> <host> 'gerrit show-connections' [--numeric | -n]
DESCRIPTION
-----------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index bf780516da..a8cf3b0300 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -41,9 +41,10 @@ EXAMPLES
SCHEMA
------
The JSON messages consist of nested objects referencing the *change*,
-*patchset*, *account* involved, and other attributes as appropriate.
+*patchSet*, *account* involved, and other attributes as appropriate.
The currently supported message types are *patchset-created*,
-*comment-added*, *change-merged*, and *change-abandoned*.
+*draft-published*, *change-abandoned*, *change-restored*,
+*change-merged*, *comment-added* and *ref-updated*.
Note that any field may be missing in the JSON messages, so consumers of
this JSON stream should deal with that appropriately.
@@ -56,7 +57,17 @@ type:: "patchset-created"
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+uploader:: link:json.html#account[account attribute]
+
+Draft Published
+^^^^^^^^^^^^^^^
+type:: "draft-published"
+
+change:: link:json.html#change[change attribute]
+
+patchset:: link:json.html#patchSet[patchset attribute]
uploader:: link:json.html#account[account attribute]
@@ -66,27 +77,31 @@ type:: "change-abandoned"
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
abandoner:: link:json.html#account[account attribute]
+reason:: Reason for abandoning the change.
+
Change Restored
-^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^
type:: "change-restored"
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
restorer:: link:json.html#account[account attribute]
+reason:: Reason for restoring the change.
+
Change Merged
^^^^^^^^^^^^^
type:: "change-merged"
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
submitter:: link:json.html#account[account attribute]
@@ -96,10 +111,12 @@ type:: "comment-added"
change:: link:json.html#change[change attribute]
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
author:: link:json.html#account[account attribute]
+approvals:: All link:json.html#approval[approval attributes] granted.
+
comment:: Comment text author had written
Ref Updated
@@ -108,7 +125,7 @@ type:: "ref-updated"
submitter:: link:json.html#account[account attribute]
-refUpdate:: link:json.html#refupdate[refupdate attribute]
+refUpdate:: link:json.html#refUpdate[refUpdate attribute]
SEE ALSO
diff --git a/Documentation/cmd-test-submit-rule.txt b/Documentation/cmd-test-submit-rule.txt
new file mode 100644
index 0000000000..5b70bd1e51
--- /dev/null
+++ b/Documentation/cmd-test-submit-rule.txt
@@ -0,0 +1,93 @@
+gerrit test-submit-rule
+=======================
+
+NAME
+----
+gerrit test-submit-rule - Test prolog submit rules with a chosen changeset.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit test-submit-rule'
+ [-s]
+ [--no-filters]
+ [--format {TEXT | JSON}]
+ CHANGE
+
+DESCRIPTION
+-----------
+Provides a way to test prolog link:prolog-cookbook.html[submit rules].
+
+OPTIONS
+-------
+-s::
+ Reads a rules.pl file from stdin instead of rules.pl in refs/meta/config.
+
+--no-filters::
+ Don't run the submit_filter/2 from the parent projects of the specified change.
+
+--format::
+ What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: A JSON object described in link:json.html#submitRecord[submit record].
+`json_compact`:: Minimized JSON output.
+--
+
+ACCESS
+------
+Can be used by anyone that has permission to read the specified changeset.
+
+EXAMPLES
+--------
+
+
+Test submit_rule from stdin.
+====
+ $ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ Non-Author-Code-Review: NOT_READY
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+Test submit_rule from stdin and return the results as JSON.
+====
+ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule --format=JSON -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ {
+ "approvals": [
+ {
+ "type": "Verified",
+ "value": "NEED"
+ },
+ {
+ "type": "Code-Review",
+ "value": "OK",
+ "by": {
+ "email": "test@email.com",
+ "username": "test"
+ }
+ }
+ ],
+ "value": "NOT_READY"
+ }
+====
+
+Test the active submit_rule from the refs/meta/config branch, ignoring filters in the project parents.
+====
+ $ ssh -p 29418 review.example.com gerrit test-submit-rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+SCRIPTING
+---------
+Can be used either interactively for testing new prolog submit rules, or from a script to check the submit status of a change.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index 5c633cfb4c..4d8851f9ab 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -48,7 +48,7 @@ person's name here, but instead some sort of organizational role.
The actual values chosen don't matter later, and are only to help
document the purpose of the key.
-Chose a fairly long expiration period, such as 20 years. For most
+Choose a fairly long expiration period, such as 20 years. For most
Gerrit instances, contact data will be written once, and rarely,
if ever, read back.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 535aaa8036..de2aa021ee 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -88,6 +88,12 @@ The default setting. Gerrit uses any valid OpenID
provider chosen by the end-user. For more information see
http://openid.net/[openid.net].
+
+* `OpenID_SSO`
++
+Supports OpenID from a single provider. There is no registration
+link, and the "Sign In" link sends the user directly to the provider's
+SSO entry point.
++
* `HTTP`
+
Gerrit relies upon data presented in the HTTP request. This includes
@@ -107,7 +113,7 @@ member of available as groups in Gerrit.
* `CLIENT_SSL_CERT_LDAP`
+
This authentication type is actually kind of SSO. Gerrit will configure
-Jetty's SSL channel to request client's SSL certificate. For this
+Jetty's SSL channel to request the client's SSL certificate. For this
authentication to work a Gerrit administrator has to import the root
certificate of the trust chain used to issue the client's certificate
into the <review-site>/etc/keystore.
@@ -161,7 +167,7 @@ By default, OpenID.
+
List of permitted OpenID providers. A user may only authenticate
with an OpenID that matches this list. Only used if `auth.type`
-was set to OpenID (the default).
+is set to OpenID (the default).
+
Patterns may be either a
link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard
@@ -173,7 +179,7 @@ allowing users to authenticate with any OpenID provider.
[[auth.trustedOpenID]]auth.trustedOpenID::
+
-List of trusted OpenID providers. Only used if `auth.type` was
+List of trusted OpenID providers. Only used if `auth.type` is
set to OpenID (the default).
+
In order for a user to take advantage of permissions beyond those
@@ -229,10 +235,17 @@ order to validate their email address expires.
+
Default is 12 hours.
+[[auth.openIdSsoUrl]]auth.openIdSsoUrl::
++
+The SSO entry point URL. Only used if `auth.type` was set to
+OpenID_SSO.
++
+The "Sign In" link will send users directly to this URL.
+
[[auth.httpHeader]]auth.httpHeader::
+
HTTP header to trust the username from, or unset to select HTTP basic
-or digest authentication. Only used if `auth.type` was set to HTTP.
+or digest authentication. Only used if `auth.type` is set to HTTP.
[[auth.logoutUrl]]auth.logoutUrl::
+
@@ -319,6 +332,18 @@ then Gerrit will do the authentication (using DIGEST authentication).
+
By default this is set to false.
+[[auth.gitBasicAuth]]auth.gitBasicAuth::
++
+If true then Git over HTTP and HTTP/S traffic is authenticated using
+standard BasicAuth and credentials validated using the same auth
+method configured for Gerrit Web UI.
++
+This parameter only affects git over http traffic. If set to false
+then Gerrit will authenticate through DIGEST authentication and
+the randomly generated HTTP password in Gerrit DB.
++
+By default this is set to false.
+
[[auth.userNameToLowerCase]]auth.userNameToLowerCase::
+
If set the username that is received to authenticate a git operation
@@ -354,8 +379,8 @@ Default is unset, no disk cache.
[[cache.name.maxAge]]cache.<name>.maxAge::
+
-Maximum age to keep an entry in the cache. If an entry has not
-been accessed in this period of time, it is removed from the cache.
+Maximum age to keep an entry in the cache. Entries are removed from
+the cache and refreshed from source data every maxAge interval.
Values should use common unit suffixes to express their setting:
+
* s, sec, second, seconds
@@ -371,7 +396,7 @@ If a unit suffix is not specified, `minutes` is assumed. If 0 is
supplied, the maximum age is infinite and items are never purged
except when the cache is full.
+
-Default is `90 days` for most caches, except:
+Default is `0`, meaning store forever with no expire, except:
+
* `"adv_bases"`: default is `10 minutes`
* `"ldap_groups"`: default is `1 hour`
@@ -379,33 +404,42 @@ Default is `90 days` for most caches, except:
[[cache.name.memoryLimit]]cache.<name>.memoryLimit::
+
-Maximum number of cache items to retain in memory. Keep in mind
-this is total number of items, not bytes of heap used.
+The total cost of entries to retain in memory. The cost computation
+varies by the cache. For most caches where the in-memory size of each
+entry is relatively the same, memoryLimit is currently defined to be
+the number of entries held by the cache (each entry costs 1).
++
+For caches where the size of an entry can vary significantly between
+individual entries (notably `"diff"`, `"diff_intraline"`), memoryLimit
+is an approximation of the total number of bytes stored by the cache.
+Larger entries that represent bigger patch sets or longer source files
+will consume a bigger portion of the memoryLimit. For these caches the
+memoryLimit should be set to roughly the amount of RAM (in bytes) the
+administrator can dedicate to the cache.
+
Default is 1024 for most caches, except:
+
* `"adv_bases"`: default is `4096`
-* `"diff"`: default is `128`
-* `"diff_intraline"`: default is `128`
+* `"diff"`: default is `10m` (10 MiB of memory)
+* `"diff_intraline"`: default is `10m` (10 MiB of memory)
+* `"plugin_resources"`: default is 2m (2 MiB of memory)
-[[cache.name.diskLimit]]cache.<name>.diskLimit::
-+
-Maximum number of cache items to retain on disk, if this cache
-supports storing its items to disk. Like memoryLimit, this is
-total number of items, not bytes of disk used. If 0, disk storage
-for this cache is disabled.
+
-Default is 16384.
+If set to 0 the cache is disabled. Entries are removed immediately
+after being stored by the cache. This is primarily useful for testing.
-[[cache.name.diskBuffer]]cache.<name>.diskBuffer::
+[[cache.name.diskLimit]]cache.<name>.diskLimit::
+
-Number of bytes to buffer in memory before writing less frequently
-accessed cache items to disk, if this cache supports storing its
-items to disk.
+Total size in bytes of the keys and values stored on disk. Caches that
+have grown bigger than this size are scanned daily at 1 AM local
+server time to trim the cache. Entries are removed in least recently
+accessed order until the cache fits within this limit. Caches may
+grow larger than this during the day, as the size check is only
+performed once every 24 hours.
+
-Default is 5 MiB.
+Default is 128 MiB per cache.
+
-Common unit suffixes of 'k', 'm', or 'g' are supported.
+If 0, disk storage for the cache is disabled.
[[cache_names]]Standard Caches
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -447,14 +481,10 @@ Each item caches the differences between two commits, at both the
directory and file levels. Gerrit uses this cache to accelerate
the display of affected file names, as well as file contents.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all changes users will view in a 1 or 2
+day span.
cache `"diff_intraline"`::
+
@@ -462,14 +492,10 @@ Each item caches the intraline difference of one file, when compared
between two commits. Gerrit uses this cache to accelerate display of
intraline differences when viewing a file.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all files users will view in a 1 or 2
+day span.
cache `"git_tags"`::
+
@@ -504,6 +530,10 @@ configured on this server. This cache should be configured with a
low maxAge setting, to ensure LDAP modifications are picked up in
a timely fashion.
+cache `"ldap_groups_byinclude"`::
++
+Caches the hierarchical structure of LDAP groups.
+
cache `"ldap_usernames"`::
+
Caches a mapping of LDAP username to Gerrit account identity. The
@@ -512,11 +542,17 @@ within Gerrit, so the cache expire time is largely irrelevant.
cache `"permission_sort"`::
+
-Caches the order access control sections must be applied to a
+Caches the order in which access control sections must be applied to a
reference. Sorting the sections can be expensive when regular
expressions are used, so this cache remembers the ordering for
each branch.
+cache `"plugin_resources"`::
++
+Caches formatted plugin resources, such as plugin documentation that
+has been converted from Markdown to HTML. The memoryLimit refers to
+the bytes of memory dedicated to storing the documentation.
+
cache `"projects"`::
+
Caches the project description records, from the `projects` table
@@ -550,8 +586,8 @@ and need to sign-in again after the restart, as the cache was
unable to persist the session information. Enabling a disk cache
is strongly recommended.
+
-Session storage is relatively inexpensive, the average entry in
-this cache is approximately 248 bytes, depending on the JVM.
+Session storage is relatively inexpensive. The average entry in
+this cache is approximately 346 bytes.
See also link:cmd-flush-caches.html[gerrit flush-caches].
@@ -563,7 +599,7 @@ cache.diff_intraline.maxIdleWorkers::
Number of idle worker threads to maintain for the intraline difference
computations. There is no upper bound on how many concurrent requests
can occur at once, if additional threads are started to handle a peak
-load, only this many will remaining idle afterwards.
+load, only this many will remain idle afterwards.
+
Default is 1.5x number of available CPUs.
@@ -639,7 +675,7 @@ will match typical Gerrit Change-Id values and create a hyperlink
to changes which reference it. The second configuration 'bugzilla'
will hyperlink terms such as 'bug 42' to an external bug tracker,
supplying the argument record number '42' for display. The third
-configuration 'tracker' uses raw HTML to more preciously control
+configuration 'tracker' uses raw HTML to more precisely control
how the replacement is displayed to the user.
----
@@ -980,6 +1016,10 @@ Default is `30 seconds`.
----
[download]
+ command = checkout
+ command = cherry_pick
+ command = pull
+ command = format_patch
scheme = ssh
scheme = http
scheme = anon_http
@@ -989,6 +1029,34 @@ Default is `30 seconds`.
The download section configures the allowed download methods.
+[[download.command]]download.command::
++
+Commands that should be offered to download changes.
++
+Multiple commands are supported:
++
+* `checkout`
++
+Command to fetch and checkout the patch set.
++
+* `cherry_pick`
++
+Command to fetch the patch set and to cherry-pick it onto the current
+commit.
++
+* `pull`
++
+Command to pull the patch set.
++
+* `format_patch`
++
+Command to fetch the patch set and to feed it into the `format-patch`
+command.
+
++
+If `download.command` is not specified, all download commands are
+offered.
+
[[download.scheme]]download.scheme::
+
Schemes that should be used to download changes.
@@ -1021,7 +1089,7 @@ generally worked on with the repo multi-repository tool. This is
not default, as not all instances will deploy repo.
+
-If download.scheme is not specified, SSH, HTTP and Anonymous HTTP
+If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
downloads are allowed.
[[gerrit]]Section gerrit
@@ -1078,10 +1146,12 @@ By default unset, as the HTTP daemon must be configured externally
by the system administrator, and might not even be running on the
same host as Gerrit.
-[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
+[[gerrit.reportBugUrl]]gerrit.reportBugUrl::
+
-If true, replicates to all remotes on startup to ensure they are
-in-sync with this server. By default, true.
+URL to direct users to when they need to report a bug about the
+Gerrit service. By default this links to the upstream Gerrit
+Code Review's own bug tracker but could be directed to the system
+administrator's ticket queue.
[[gitweb]]Section gitweb
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1187,6 +1257,11 @@ Optional path to hooks, if not specified then `'$site_path'/hooks` will be used.
Optional filename for the patchset created hook, if not specified then
`patchset-created` will be used.
+[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
++
+Optional filename for the draft published hook, if not specified then
+`draft-published` will be used.
+
[[hooks.commentAddedHook]]hooks.commentAddedHook::
+
Optional filename for the comment added hook, if not specified then
@@ -1202,6 +1277,21 @@ Optional filename for the change merged hook, if not specified then
Optional filename for the change abandoned hook, if not specified then
`change-abandoned` will be used.
+[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
++
+Optional filename for the change restored hook, if not specified then
+`change-restored` will be used.
+
+[[hooks.refUpdatedHook]]hooks.refUpdatedHook::
++
+Optional filename for the ref updated hook, if not specified then
+`ref-updated` will be used.
+
+[[hooks.claSignedHook]]hooks.claSignedHook::
++
+Optional filename for the CLA signed hook, if not specified then
+`cla-signed` will be used.
+
[[http]]Section http
~~~~~~~~~~~~~~~~~~~~
@@ -1325,7 +1415,7 @@ By default, `$site_path/etc/keystore`.
[[httpd.sslKeyPassword]]httpd.sslKeyPassword::
+
Password used to decrypt the private portion of the sslKeyStore.
-Java key stores require a password, even if the administrator
+Java keystores require a password, even if the administrator
doesn't want to enable one.
+
If set to the empty string the embedded server will prompt for the
@@ -1345,7 +1435,7 @@ and false if httpd.listenUrl uses proxy-http:// or proxy-https://.
[[httpd.acceptorThreads]]httpd.acceptorThreads::
+
Number of worker threads dedicated to accepting new incoming TCP
-connections and allocate them connection-specific resources.
+connections and allocating them connection-specific resources.
+
By default, 2, which should be suitable for most high-traffic sites.
@@ -1373,7 +1463,7 @@ By default 50.
[[httpd.maxWait]]httpd.maxWait::
+
-Maximum amount of time a client will wait to for an available
+Maximum amount of time a client will wait for an available
thread to handle a project clone, fetch or push request over the
smart HTTP transport.
+
@@ -1398,7 +1488,7 @@ By default, 5 minutes.
[[ldap]]Section ldap
~~~~~~~~~~~~~~~~~~~~
-LDAP integration is only enabled if `auth.type` was set to
+LDAP integration is only enabled if `auth.type` is set to
`HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`. See above for a
detailed description of the auth.type settings and their
implications.
@@ -1466,7 +1556,7 @@ By default, `ignore`.
_(Optional)_ The read timeout for an LDAP operation. The value is
in the usual time-unit format like "1 s", "100 ms", etc...
A timeout can be used to avoid blocking all of the SSH command start
-threads in case when the LDAP server becomes slow.
+threads in case the LDAP server becomes slow.
+
By default there is no timeout and Gerrit will wait for the LDAP
server to respond until the TCP connection times out.
@@ -1513,8 +1603,8 @@ contains the initial value for the user's full name field in Gerrit.
Typically this is the `displayName` property in LDAP, but could
also be `legalName` or `cn`.
+
-Attribute values may be concatenated with literal strings, for
-example to join given name and surname together use the pattern
+Attribute values may be concatenated with literal strings. For
+example to join given name and surname together, use the pattern
`${givenName} ${SN}`.
+
If set, users will be unable to modify their full name field, as
@@ -1535,7 +1625,7 @@ of sAMAccountName followed by a constant domain name, use
`${sAMAccountName.toLowerCase}@example.com`.
+
If set, the preferred email address will be prefilled from LDAP,
-but users may still be able to register additional email address,
+but users may still be able to register additional email addresses,
and select a different preferred email address.
+
Default is `mail`.
@@ -1625,7 +1715,7 @@ can be achieved.
+
If set, it must be ensured that the local usernames for all existing
accounts are converted to lower case, otherwise a user that has a
-local username that contains upper case characters cannot login
+local username that contains upper case characters will not be able to login
anymore. The local usernames for the existing accounts can be
converted to lower case by running the server program
link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase].
@@ -1689,6 +1779,22 @@ auto-detected and one thread per CPU is used, per client request.
By default, 1.
+[[plugins]]Section plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[plugins.checkFrequency]]plugins.checkFrequency::
++
+How often plugins should be examined for new plugins to load, removed
+plugins to be unloaded, or updated plugins to be reloaded. Values can
+be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+If set to 0, automatic plugin reloading is disabled. Administrators
+may force reloading with link:cmd-plugin.html[gerrit plugin reload].
++
+Default is 1 minute.
+
+
[[receive]]Section receive
~~~~~~~~~~~~~~~~~~~~~~~~~~
This section is used to set who can execute the 'receive-pack' and
@@ -1719,7 +1825,7 @@ If an object is larger than the given size the pack-parsing will abort
and the push operation will fail. If set to zero then there is no
limit.
+
-Gerrit administrator can use this setting to prevent developers
+Gerrit administrators can use this setting to prevent developers
from pushing objects which are too large to Gerrit.
+
Default is zero.
@@ -1870,6 +1976,22 @@ email from Gerrit.
+
By default, unset, permitting delivery to any email address.
+[[sendemail.includeDiff]]sendemail.includeDiff::
++
+If true, new change emails from Gerrit will include the complete
+unified diff of the change. Variable maxmimumDiffSize places an upper
+limit on how large the email can get when this option is enabled.
++
+By default, false.
+
+[[sendemail.maximumDiffSize]]sendemail.maximumDiffSize::
++
+Largest size of unified diff output to include in an email. When
+the diff exceeds this size the file paths will be listed instead.
+Standard byte unit suffixes are supported.
++
+By default, 256 KiB.
+
[[sendemail.importance]]sendemail.importance::
+
If present, emails sent from Gerrit will have the given level
@@ -1907,6 +2029,45 @@ If true the server checks the site header, footer and CSS files for
updated versions. If false, a server restart is required to change
any of these resources. Default is true, allowing automatic reloads.
+[[site.enableDeprecatedQuery]]site.enableDeprecatedQuery::
++
+If true the deprecated `/query` URL is available to return JSON
+and text results for changes. If false, the URL is disabled and
+returns 404 to clients. Default is true, enabling `/query`.
+
+[[site.upgradeSchemaOnStartup]]site.upgradeSchemaOnStartup::
++
+Control whether schema upgrade should be done on Gerrit startup. The following
+values are supported:
++
+* `OFF`
++
+No automatic schema upgrade on startup.
++
+* `AUTO`
++
+Perform schema migration on startup, if necessary. If, as a result of
+schema migration, there would be any unused database objects they will
+be dropped automatically.
++
+* `AUTO_NO_PRUNE`
++
+Like `AUTO` but unused database objects will not be pruned.
+
++
+The default is `OFF`.
+
+[[ssh-alias]] Section ssh-alias
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Variables in section ssh-alias permit the site administrator to alias
+another command from Gerrit or a plugin into the `gerrit` command
+namespace. To alias `replication start` to `gerrit replicate`:
+
+----
+[ssh-alias]
+ replicate = replication start
+----
[[sshd]] Section sshd
~~~~~~~~~~~~~~~~~~~~~
@@ -1969,7 +2130,7 @@ By default, true.
+
Number of threads to use when executing SSH command requests.
If additional requests are received while all threads are busy they
-are queued and serviced in a first-come-first-serve order.
+are queued and serviced in a first-come-first-served order.
+
By default, 1.5x the number of CPUs available to the JVM.
@@ -2036,7 +2197,7 @@ By default, 2 minutes.
+
Maximum number of concurrent SSH sessions that a user account
may open at one time. This is the number of distinct SSH logins
-the each user may have active at one time, and is not related to
+that each user may have active at one time, and is not related to
the number of commands a user may issue over a single connection.
If set to 0, there is no limit.
+
@@ -2132,6 +2293,31 @@ code, or standard color name.
+
By default a shade of yellow, `FFFFCC`.
+[[theme.changeTableOutdatedColor]]theme.changeTableOutdatedColor::
++
+Background color used for patch outdated messages. The value must be
+a valid HTML hex color code, or standard color name.
++
+By default a shade of red, `F08080`.
+
+[[theme.tableOddRowColor]]theme.tableOddRowColor::
++
+Background color for tables such as lists of open reviews for odd
+rows. This is so you can have a different color for odd and even
+rows of the table. The value must be a valid HTML hex color code,
+or standard color name.
++
+By default transparent.
+
+[[theme.tableEvenRowColor]]theme.tableEvenRowColor::
++
+Background color for tables such as lists of open reviews for even
+rows. This is so you can have a different color for odd and even
+rows of the table. The value must be a valid HTML hex color code,
+or standard color name.
++
+By default transparent.
+
A different theme may be used for signed-in vs. signed-out user status
by using the "signed-in" and "signed-out" theme sections. Variables
not specified in a section are inherited from the default theme.
@@ -2184,7 +2370,7 @@ Java regular expression (java.util.regex)] used to match the
external tracking id part of the footer line. The match can
result in several entries in the DB. If grouping is used in the
regex the first group will be interpreted as the tracking id.
-Tracking ids > 20 char will be ignored.
+Tracking ids longer than 20 characters will be ignored.
+
The configuration file parser eats one level of backslashes, so the
character class `\s` requires `\\s` in the configuration file. The
@@ -2193,7 +2379,7 @@ expression containing # must be wrapped in double quotes.
[[trackingid.name.system]]trackingid.<name>.system::
+
-The name of the external tracking system(max 10 char).
+The name of the external tracking system (maximum 10 characters).
It is possible to have several trackingid entries for the same
tracking system.
@@ -2276,6 +2462,7 @@ Sample `etc/secure.config`:
----
[auth]
registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
+ restTokenPrivateKey = 7e40PzCjlUKOnXATvcBNXH6oyiu+r0dFk2c=
[database]
username = webuser
@@ -2294,15 +2481,6 @@ Sample `etc/secure.config`:
password = s3kr3t
----
-File `etc/replication.config`
------------------------------
-
-The optional file `'$site_path'/etc/replication.config` controls how
-Gerrit automatically replicates changes it makes to any of the Git
-repositories under its control.
-
-* link:config-replication.html[Git Replication/Mirroring]
-
File `etc/peer_keys`
--------------------
@@ -2338,7 +2516,6 @@ Files in this directory provide additional configuration.
Other files support site customization.
+
* link:config-headerfooter.html[Site Header/Footer]
-* link:config-replication.html[Git Replication/Mirroring]
GERRIT
------
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index a08eb873b8..35d5c0d7e5 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -27,7 +27,7 @@ Linux distributions.
Alternatively, if Gerrit is served behind reverse proxy, it can
generate different URLs for gitweb's links (they need to be
rewritten to `<gerrit>/gitweb?args` on the web server). This allows
-for serving gitweb under different URL than the Gerrit instance.
+for serving gitweb under a different URL than the Gerrit instance.
To enable this feature, set both: `gitweb.cgi` and `gitweb.url`.
====
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-headerfooter.txt
index c06080b50b..ae5d8f7eb5 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-headerfooter.txt
@@ -42,7 +42,7 @@ and may be referenced in `GerritSite{Header,Footer}.html`
or `GerritSite.css` by the relative URL `static/$name`
(e.g. `static/logo.png`).
-To simplify security management, only files are served from
+To simplify security management, files are only served from
`'$site_path'/static`. Subdirectories are explicitly forbidden from
being served from this location by enforcing the rule that file names
cannot contain `/` or `\`. (Client requests for `static/foo/bar`
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index ceb7c78ec5..dfdba523d6 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -24,10 +24,19 @@ patchset-created
~~~~~~~~~~~~~~~~
This is called whenever a patchset is created (this includes new
-changes)
+changes and drafts).
====
- patchset-created --change <change id> --change-url <change url> --project <project name> --branch <branch> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+ patchset-created --change <change id> --is-draft <boolean> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+====
+
+draft-published
+~~~~~~~~~~~~~~~
+
+This is called whenever a draft change is published.
+
+====
+ draft-published --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
====
comment-added
@@ -36,7 +45,7 @@ comment-added
This is called whenever a comment is added to a change.
====
- comment-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
+ comment-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
====
change-merged
@@ -45,7 +54,7 @@ change-merged
Called whenever a change has been merged.
====
- change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --submitter <submitter> --commit <sha1>
+ change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
====
change-abandoned
@@ -54,16 +63,16 @@ change-abandoned
Called whenever a change has been abandoned.
====
- change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason>
+ change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --reason <reason>
====
change-restored
-~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~
Called whenever a change has been restored.
====
- change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason>
+ change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --reason <reason>
====
ref-updated
@@ -76,9 +85,9 @@ Called whenever a ref has been updated.
====
cla-signed
-~~~~~~~~~~~
+~~~~~~~~~~
-Called whenever a user signs a contributor license agreement
+Called whenever a user signs a contributor license agreement.
====
cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
@@ -88,13 +97,15 @@ Called whenever a user signs a contributor license agreement
Configuration Settings
----------------------
-It is possible to change where gerrit looks for hooks, and what
-filenames it looks for by adding a [hooks] section to gerrit.config.
+It is possible to change where Gerrit looks for hooks, and what
+filenames it looks for, by adding a [hooks] section in gerrit.config.
+
+Gerrit will use the value of hooks.path for the hooks directory.
-Gerrit will use the value of hooks.path for the hooks directory, and
-the values of hooks.patchsetCreatedHook, hooks.commentAddedHook,
-hooks.changeMergedHook and hooks.changeAbandonedHook for the
-filenames for the hooks.
+For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
+hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
+hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook and
+hooks.claSignedHook.
Missing Change URLs
-------------------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8aa7d08225..ad0704f0f6 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -20,7 +20,7 @@ and modifying it will allow an administrator to customize the template.
Supported Mail Templates:
-------------------------
-Each mail that Gerrit sends out is controlled by at least one template, these
+Each mail that Gerrit sends out is controlled by at least one template. These
are listed below. Change emails are influenced by two additional templates,
one to set the subject line, and one to set the footer which gets appended to
all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.)
@@ -36,7 +36,7 @@ ChangeFooter.vm
~~~~~~~~~~~~~~~
The `ChangeFooter.vm` template will determine the contents of the footer
-text that will be appended to emails related to changes (all `ChangeEmails)`.
+text that will be appended to emails related to changes (all `ChangeEmail`s).
ChangeSubject.vm
~~~~~~~~~~~~~~~~
@@ -49,6 +49,7 @@ Comment.vm
The `Comment.vm` template will determine the contents of the email related to
a user submitting comments on changes. It is a `ChangeEmail`: see
+`ChangeSubject.vm` and `ChangeFooter.vm`.
Merged.vm
~~~~~~~~~
@@ -62,6 +63,7 @@ MergeFail.vm
The `MergeFail.vm` template will determine the contents of the email related
to a failure upon attempting to merge a change to the head. It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
NewChange.vm
~~~~~~~~~~~~
@@ -70,6 +72,13 @@ The `NewChange.vm` template will determine the contents of the email related
to a user submitting a new change for review. It is a `ChangeEmail`: see
`ChangeSubject.vm` and `ChangeFooter.vm`.
+RebasedPatchSet.vm
+~~~~~~~~~~~~~~~~~~
+
+The `RebasedPatchSet.vm` template will determine the contents of the email
+related to a user rebasing a patchset for a change through the Gerrit UI.
+It is a `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+
RegisterNewEmail.vm
~~~~~~~~~~~~~~~~~~~
@@ -90,6 +99,12 @@ The `Restored.vm` template will determine the contents of the email related
to a change being restored. It is a `ChangeEmail`: see `ChangeSubject.vm` and
`ChangeFooter.vm`.
+Reverted.vm
+~~~~~~~~~~~
+
+The `Reverted.vm` template will determine the contents of the email related
+to a change being reverted. It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
Mail Variables and Methods
@@ -107,7 +122,7 @@ be necessary for anything more than a minor formatting change.
Warning
~~~~~~~
-Be aware that modifying templates can cause them to fail to parse and therefor
+Be aware that modifying templates can cause them to fail to parse and therefore
not send out the actual email, or worse, calling methods on the available
objects could have internal side effects which would adversely affect the
health of your Gerrit server and/or data.
@@ -125,7 +140,7 @@ or the current child class inherited from it.
$messageClass::
+
-A String containing the messageClass
+A String containing the messageClass.
$StringUtils::
+
@@ -139,35 +154,35 @@ All change related emails have the following additional variables available to t
$change::
+
-A reference to the current `Change` object
+A reference to the current `Change` object.
$changeId::
+
-Id of the current change (a `Change.Key`)
+Id of the current change (a `Change.Key`).
$coverLetter::
+
-The text of the `ChangeMessage`
+The text of the `ChangeMessage`.
$branch::
+
-A reference to the branch of this change (a `Branch.NameKey`)
+A reference to the branch of this change (a `Branch.NameKey`).
$fromName::
+
-The name of the from user
+The name of the from user.
$projectName::
+
-The name of this change's project
+The name of this change's project.
$patchSet::
+
-A reference to the current `PatchSet`
+A reference to the current `PatchSet`.
$patchSetInfo::
+
-A reference to the current `PatchSetInfo`
+A reference to the current `PatchSetInfo`.
See Also
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
deleted file mode 100644
index 772282e7de..0000000000
--- a/Documentation/config-replication.txt
+++ /dev/null
@@ -1,273 +0,0 @@
-Gerrit Code Review - Git Replication
-====================================
-
-Gerrit can automatically push any changes it makes to its managed Git
-repositories to another system. Usually this would be configured to
-provide mirroring of changes, for warm-standby backups, or a
-load-balanced public mirror farm.
-
-The replication runs on a short delay. This gives Gerrit a small
-time window to batch updates going to the same project, such as
-when a user uploads multiple changes at once.
-
-Typically replication should be done over SSH, with a passwordless
-public/private key pair. On a trusted network it is also possible to
-use replication over the insecure (but much faster) git:// protocol,
-by enabling the `receive-pack` service on the receiving system, but
-this configuration is not recommended. It is also possible to
-specify a local path as replication target. This makes e.g. sense if
-a network share is mounted to which the repositories should be
-replicated.
-
-Enabling Replication
---------------------
-
-If replicating over SSH (recommended), ensure the host key of the
-remote system(s) is already in the Gerrit user's `~/.ssh/known_hosts`
-file. The easiest way to add the host key is to connect once by hand
-with the command line:
-
-====
- sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
-====
-
-Next, create `'$site_path'/etc/replication.config` as a Git-style
-config file, and restart Gerrit.
-
-Example `replication.config` to replicate in parallel to four
-different hosts:
-
-====
- [remote "host-one"]
- url = gerrit2@host-one.example.com:/some/path/${name}.git
-
- [remote "pubmirror"]
- url = mirror1.us.some.org:/pub/git/${name}.git
- url = mirror2.us.some.org:/pub/git/${name}.git
- url = mirror3.us.some.org:/pub/git/${name}.git
- push = +refs/heads/*:refs/heads/*
- push = +refs/tags/*:refs/tags/*
- threads = 3
- authGroup = Public Mirror Group
- authGroup = Second Public Mirror Group
-====
-
-To manually trigger replication at runtime, see
-link:cmd-replicate.html[gerrit replicate].
-
-[[replication_config]]File `replication.config`
------------------------------------------------
-
-The optional file `'$site_path'/etc/replication.config` is a
-Git-style config file that controls the replication settings for
-Gerrit.
-
-The file is composed of one or more `remote` sections, each remote
-section provides common configuration settings for one or more
-destination URLs.
-
-Each remote section uses its own thread pool. If pushing to
-multiple remotes, over differing types of network connections
-(e.g. LAN and also public Internet), its a good idea to put them
-into different remote sections, so that replication to the slower
-connection does not starve out the faster local one. The example
-file above does this.
-
-[[remote]]Section remote
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the keys below, the <name> portion is unused by Gerrit, but must be
-unique to distinguish the different sections if more than one remote
-section appears in the file.
-
-[[remote.name.url]]remote.<name>.url::
-+
-Address of the remote server to push to. Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings. Assuming sufficient
-threads in the thread pool, Gerrit pushes to all URLs in parallel,
-using one thread per URL.
-+
-Within each URL value the magic placeholder `${name}` is replaced
-with the Gerrit project name. This is a Gerrit specific extension
-to the otherwise standard Git URL syntax and it must be included
-in each URL so that Gerrit can figure out where each project needs
-to be replicated.
-+
-See link:http://www.kernel.org/pub/software/scm/git/docs/git-push.html#URLS[GIT URLS]
-for details on Git URL syntax.
-
-[[remote.name.url]]remote.<name>.adminUrl::
-+
-Address of the alternative remote server only for repository creation. Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.
-+
-The adminUrl can be used as a ssh alternative to the url option, but only related to repository creation.
-If not specified, the repository creation tries to follow the default way through the url value specified.
-+
-It is useful when remote.<name>.url protocols does not allow repository creation
-although their usage are mandatory in the local environment.
-In that case, an alternative ssh url could be specified to repository creation.
-
-[[remote.name.receivepack]]remote.<name>.receivepack::
-+
-Path of the `git-receive-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-receive-pack`.
-
-[[remote.name.uploadpack]]remote.<name>.uploadpack::
-+
-Path of the `git-upload-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-upload-pack`.
-
-[[remote.name.push]]remote.<name>.push::
-+
-Standard Git refspec denoting what should be replicated. Setting this
-to `+refs/heads/*:refs/heads/*` would mirror only the active
-branches, but not the change refs under `refs/changes/`, or the tags
-under `refs/tags/`.
-+
-Multiple push keys can be supplied, to specify multiple patterns
-to match against. In the example file above, remote "pubmirror"
-uses two push keys to match both `refs/heads/*` and `refs/tags/*`,
-but excludes all others, including `refs/changes/*`.
-+
-Defaults to `+refs/*:refs/*` (all refs) if not specified.
-
-[[remote.name.timeout]]remote.<name>.timeout::
-+
-Number of seconds to wait for a network read or write to complete
-before giving up and declaring the remote side is not responding.
-If 0, there is no timeout, and the push client waits indefinitely.
-+
-A timeout should be large enough to mostly transfer the objects to
-the other side. 1 second may be too small for larger projects,
-especially over a WAN link, while 10-30 seconds is a much more
-reasonable timeout value.
-+
-Defaults to 0 seconds, wait indefinitely.
-
-[[remote.name.replicationDelay]]remote.<name>.replicationDelay::
-+
-Number of seconds to wait before scheduling a remote push operation.
-Setting the delay to 0 effectively disables the delay, causing the
-push to start as soon as possible.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 15 seconds.
-
-[[remote.name.replicationRetry]]remote.<name>.replicationRetry::
-+
-Number of minutes to wait before scheduling a remote push operation
-previously failed due to an offline remote server.
-+
-If a remote push operation fails because a remote server was
-offline, all push operations to the same destination URL are
-blocked, and the remote push is continuously retried.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 1 minute.
-
-[[remote.name.threads]]remote.<name>.threads::
-+
-Number of worker threads to dedicate to pushing to the repositories
-described by this remote. Each thread can push one project at a
-time, to one destination URL. Scheduling within the thread pool
-is done on a per-project basis. If a remote block describes 4
-URLs, allocating 4 threads in the pool will permit some level of
-parallel pushing.
-+
-By default, 1 thread.
-
-[[remote.name.authGroup]]remote.<name>.authGroup::
-+
-Specifies the name of a group that the remote should use to access
-the repositories. Multiple authGroups may be specified within a
-single remote block to signify a wider access right. In the project
-administration web interface the read access can be specified for
-this group to control if a project should be replicated or not to the
-remote.
-+
-By default, replicates without group control, i.e replicates
-everything to all remotes.
-
-[[remote.name.replicatePermissions]]remote.<name>.replicatePermissions::
-+
-If true, permissions-only projects and the refs/meta/config branch
-will also be replicated to the remote site. These projects and
-branches may be needed to keep a backup or slave server current.
-+
-By default, true, replicating everything.
-
-[[remote.name.mirror]]remote.<name>.mirror::
-+
-If true, replication will remove remote branches that absent locally
-or invisible to the replication (i.e. read access denied via 'authGroup'
-option).
-+
-By default, false, do not remove remote branches.
-
-
-[[secure_config]]File `secure.config`
------------------------------------------------
-
-The optional file `'$site_path'/secure.config` is a Git-style config
-file that provides secure values that should not be world-readable,
-such as passwords. Passwords for HTTP remotes can be obtained from
-this file.
-
-[[remote.name.username]]remote.<name>.username::
-+
-Username to use for HTTP authentication on this remote, if not given
-in the URL.
-
-[[remote.name.password]]remote.<name>.password::
-+
-Password to use for HTTP authentication on this remote.
-
-
-[[ssh_config]]File `~/.ssh/config`
-----------------------------------
-
-If present, Gerrit reads and caches `~/.ssh/config` at startup, and
-supports most SSH configuration options. For example:
-
-====
- Host host-one.example.com:
- IdentityFile ~/.ssh/id_hostone
- PreferredAuthentications publickey
-
- Host mirror*.us.some.org:
- User mirror-updater
- IdentityFile ~/.ssh/id_pubmirror
- PreferredAuthentications publickey
-====
-
-Supported options:
-
- * Host
- * Hostname
- * User
- * Port
- * IdentityFile
- * PreferredAuthentications
- * StrictHostKeyChecking
-
-SSH authentication must be by passwordless public key, as there is
-no facility to read passphases on startup or passwords during the
-SSH connection setup, and SSH agents are not supported from Java.
-
-Host keys for any destination SSH servers must appear in the user's
-`~/.ssh/known_hosts` file, and must be added in advance, before
-Gerrit starts. If a host key is not listed, Gerrit will be unable to
-connect to that destination, and replication to that URL will fail.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 4eecf67018..7161c4a223 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -5,7 +5,7 @@ Description
-----------
Gerrit can be configured to run behind a third-party web server.
-This allows the other web server to bind to the privileged ports 80
+This allows the other web server to bind to the privileged port 80
(or 443 for SSL), as well as offloads the SSL processing overhead
from Java to optimized native C code.
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 9aa06bedea..e915ffb8a8 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -146,7 +146,7 @@ To enable this form of authentication:
The auth.type must always be HTTP, indicating the user identity
will be obtained from the HTTP authorization data.
-The auth.httpHeader indicates which HTTP header field the Siteminder
+The auth.httpHeader indicates in which HTTP header field the Siteminder
product has stored the username. Usually this is "SM_USER", but
may differ in your environment. Please refer to your organization's
single sign-on or security group to ensure the setting is correct.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 2609b05041..065e9d115d 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -150,7 +150,7 @@ Here are some guidelines that Gerrit uses:
should be before the instance members.
* Annotations should go before language keywords (final, private...) +
Example: @Assisted @Nullable final type varName
- * Imports should be mostly aphabetical (uppercase sorts before
+ * Imports should be mostly alphabetical (uppercase sorts before
all lowercase, which means classes come before packages at the
same level).
@@ -164,7 +164,7 @@ back and consult this section when creating them.
Design
------
-Here are some design level ojectives that you should keep in mind
+Here are some design level objectives that you should keep in mind
when coding:
* ORM entity objects should match exactly one row in the database.
@@ -191,6 +191,7 @@ when coding:
on slow links. If the action buttons are disabled, they cannot
be resubmitted and the user can see that Gerrit is still busy.
* GWT EventBus is the new way forward.
+ * ...and so is Guava (previously known as Google Collections).
Tests
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index bf5ba73c46..ce2868c58e 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -37,7 +37,7 @@ does not enforce peer-review prior to submission.
Git is a distributed version control system, wherein each repository
is assumed to be owned/maintained by a single user. There are no
-inherit security controls built into Git, so the ability to read
+inherent security controls built into Git, so the ability to read
from or write to a repository is controlled entirely by the host's
filesystem access controls. When multiple maintainers collaborate
on a single shared repository a high degree of trust is required,
@@ -87,7 +87,7 @@ and Git's own data integrity checks.
Each Git commit created on the client desktop system is converted
into a unique change record which can be reviewed independently.
-Change records are stored in a database: PostgreSQL, MySql, or the
+Change records are stored in a database: PostgreSQL, MySQL, or the
built-in H2, where they can be queried to present customized user
dashboards, enumerating any pending changes.
@@ -191,7 +191,7 @@ Gerrit is developed as a self-hosting open source project:
* link:http://code.google.com/p/gerrit/[Project Homepage]
* link:http://code.google.com/p/gerrit/downloads/list[Release Versions]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source]
+* link:http://code.google.com/p/gerrit/source/checkout[Source]
* link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
* link:https://review.source.android.com/[Change Review]
@@ -669,17 +669,18 @@ lag largely allows for some downtime in a disaster scenario.
Backups
~~~~~~~
-PostgreSQL can be configured to save its write-ahead-log (WAL)
-and ship these logs to other systems, where they are applied to
-a warm-standby backup in real time. Gerrit instances which care
-about reduduncy will setup this feature of PostgreSQL to ensure
-the warm-standby is reasonably current should the master go offline.
-
-Gerrit can be configured to replicate changes made to the local
-Git repositories over any standard Git transports. This can be
-configured in `'$site_path'/etc/replication.conf` to send copies
-of all changes over SSH to other servers, or to the Amazon S3 blob
-storage service.
+PostgreSQL and MySQL can be configured to replicate their data to
+other systems, where they are applied to a warm-standby backup in
+real time. Gerrit instances which care about reduduncy will setup
+this feature of PostgreSQL or MySQL to ensure the warm-standby is
+reasonably current should the master go offline.
+
+Using the standard replication plugin, Gerrit can be configured
+to replicate changes made to the local Git repositories over any
+standard Git transports. After the plugin is installed, remote
+destinations can be configured in `'$site_path'/etc/replication.conf`
+to send copies of all changes over SSH to other servers, or to the
+Amazon S3 blob storage service.
Logging Plan
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index e239a63edc..b2bf0119dc 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -70,12 +70,19 @@ Duplicate the existing `pgm_daemon` launch configuration:
* Change Save as to be Local file.
+[[hosted-mode]]
Running Hosted Mode
~~~~~~~~~~~~~~~~~~~
-Import the gerrit-gwtdebug project:
+To debug the GWT code executing in the web browser, three additional Git
+repositories need to be cloned.
-* Import gerrit-gwtdebug/pom.xml using General -> Maven Projects
+* https://gerrit.googlesource.com/gwtexpui
+* https://gerrit.googlesource.com/gwtjsonrpc
+* https://gerrit.googlesource.com/gwtorm
+
+In Eclipse, import the pom.xml file in the root directory of each of
+these cloned gits via General -> Maven Projects.
Duplicate the existing `gwtui_dbg` launch configuration:
@@ -94,6 +101,22 @@ Duplicate the existing `gwtui_dbg` launch configuration:
* Change Save as to be Local file.
+[[known-problems]]
+Known problems
+--------------
+
+* When running Gerrit under the Eclipse debugger, code that attempts
+to load Prolog code may erroneously raise ClassNotFoundException,
+claiming that classes in the `Gerrit` package can't be found. The
+error can often be resolved by rebuilding Gerrit with `mvn package`
+and restarting the debug session.
+
+* OpenID authentication won't work in hosted mode, so you need to change
+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
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
new file mode 100644
index 0000000000..dd0c44c70f
--- /dev/null
+++ b/Documentation/dev-plugins.txt
@@ -0,0 +1,420 @@
+Gerrit Code Review - Plugin Development
+=======================================
+
+The Gerrit server functionality can be extended by installing plugins.
+This page describes how plugins for Gerrit can be developed.
+
+Depending on how tightly the extension code is coupled with the Gerrit
+server code, there is a distinction between `plugins` and `extensions`.
+
+[[plugin]]
+A `plugin` in Gerrit is tightly coupled code that runs in the same
+JVM as Gerrit. It has full access to all server internals. Plugins
+are tightly coupled to a specific major.minor server version and
+may require source code changes to compile against a different
+server version.
+
+[[extension]]
+An `extension` in Gerrit runs inside of the same JVM as Gerrit
+in the same way as a plugin, but has limited visibility to the
+server's internals. The limited visibility reduces the extension's
+dependencies, enabling it to be compatible across a wider range
+of server versions.
+
+Most of this documentation refers to either type as a plugin.
+
+[[getting-started]]
+Getting started
+---------------
+
+To get started with the development of a plugin there are two
+recommended ways:
+
+. use the Gerrit Plugin Maven archetype to create a new plugin project:
++
+With the Gerrit Plugin Maven archetype you can create a skeleton for a
+plugin project.
++
+----
+mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
+ -DarchetypeArtifactId=gerrit-plugin-archetype \
+ -DarchetypeVersion=2.5-SNAPSHOT \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=testPlugin
+----
++
+Maven will ask for additional properties and then create the plugin in
+the current directory. To change the default property values answer 'n'
+when Maven asks to confirm the properties configuration. It will then
+ask again for all properties including those with predefined default
+values.
+
+. clone the sample helloworld plugin:
++
+This is a Maven project that adds an SSH command to Gerrit to print
+out a hello world message. It can be taken as an example to develop
+an own plugin.
++
+----
+$ git clone https://gerrit.googlesource.com/plugins/helloworld
+----
++
+When starting from this example one should take care to adapt the
+`Gerrit-ApiVersion` in the `pom.xml` to the version of Gerrit for which
+the plugin is developed. If the plugin is developed for a released
+Gerrit version (no `SNAPSHOT` version) then the URL for the
+`gerrit-api-repository` in the `pom.xml` needs to be changed to
+`https://gerrit-api.commondatastorage.googleapis.com/release/`.
+
+[[API]]
+API
+---
+
+There are two different API formats offered against which plugins can
+be developed:
+
+gerrit-extension-api.jar::
+ A stable but thin interface. Suitable for extensions that need
+ to be notified of events, but do not require tight coupling to
+ the internals of Gerrit. Extensions built against this API can
+ expect to be binary compatible across a wide range of server
+ versions.
+
+gerrit-plugin-api.jar::
+ The complete internals of the Gerrit server, permitting a
+ plugin to tightly couple itself and provide additional
+ functionality that is not possible as an extension. Plugins
+ built against this API are expected to break at the source
+ code level between every major.minor Gerrit release. A plugin
+ that compiles against 2.5 will probably need source code level
+ changes to work with 2.6, 2.7, and so on.
+
+Manifest
+--------
+
+Plugins may provide optional description information with standard
+manifest fields:
+
+====
+ Implementation-Title: Example plugin showing examples
+ Implementation-Version: 1.0
+ Implementation-Vendor: Example, Inc.
+ Implementation-URL: http://example.com/opensource/plugin-foo/
+====
+
+ApiType
+~~~~~~~
+
+Plugins using the tightly coupled `gerrit-plugin-api.jar` must
+declare this API dependency in the manifest to gain access to server
+internals. If no `Gerrit-ApiType` is specified the stable `extension`
+API will be assumed. This may cause ClassNotFoundExceptions when
+loading a plugin that needs the plugin API.
+
+====
+ Gerrit-ApiType: plugin
+====
+
+Explicit Registration
+~~~~~~~~~~~~~~~~~~~~~
+
+Plugins that use explicit Guice registration must name the Guice
+modules in the manifest. Up to three modules can be named in the
+manifest. `Gerrit-Module` supplies bindings to the core server;
+`Gerrit-SshModule` supplies SSH commands to the SSH server (if
+enabled); `Gerrit-HttpModule` supplies servlets and filters to the HTTP
+server (if enabled). If no modules are named automatic registration
+will be performed by scanning all classes in the plugin JAR for
+`@Listen` and `@Export("")` annotations.
+
+====
+ Gerrit-Module: tld.example.project.CoreModuleClassName
+ Gerrit-SshModule: tld.example.project.SshModuleClassName
+ Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+====
+
+[[reload_method]]
+Reload Method
+~~~~~~~~~~~~~
+
+If a plugin holds an exclusive resource that must be released before
+loading the plugin again (for example listening on a network port or
+acquiring a file lock) the manifest must declare `Gerrit-ReloadMode`
+to be `restart`. Otherwise the preferred method of `reload` will
+be used, as it enables the server to hot-patch an updated plugin
+with no down time.
+
+====
+ Gerrit-ReloadMode: restart
+====
+
+In either mode ('restart' or 'reload') any plugin or extension can
+be updated without restarting the Gerrit server. The difference is
+how Gerrit handles the upgrade:
+
+restart::
+ The old plugin is completely stopped. All registrations of SSH
+ commands and HTTP servlets are removed. All registrations of any
+ extension points are removed. All registered LifecycleListeners
+ have their `stop()` method invoked in reverse order. The new
+ plugin is started, and registrations are made from the new
+ plugin. There is a brief window where neither the old nor the
+ new plugin is connected to the server. This means SSH commands
+ and HTTP servlets will return not found errors, and the plugin
+ will not be notified of events that occurred during the restart.
+
+reload::
+ The new plugin is started. Its LifecycleListeners are permitted
+ to perform their `start()` methods. All SSH and HTTP registrations
+ are atomically swapped out from the old plugin to the new plugin,
+ ensuring the server never returns a not found error. All extension
+ point listeners are atomically swapped out from the old plugin to
+ the new plugin, ensuring no events are missed (however some events
+ may still route to the old plugin if the swap wasn't complete yet).
+ The old plugin is stopped.
+
+To reload/restart a plugin the link:cmd-plugin-reload.html[plugin reload]
+command can be used.
+
+[[classpath]]
+Classpath
+---------
+
+Each plugin is loaded into its own ClassLoader, isolating plugins
+from each other. A plugin or extension inherits the Java runtime
+and the Gerrit API chosen by `Gerrit-ApiType` (extension or plugin)
+from the hosting server.
+
+Plugins are loaded from a single JAR file. If a plugin needs
+additional libraries, it must include those dependencies within
+its own JAR. Plugins built using Maven may be able to use the
+link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin]
+to package additional dependencies. Relocating (or renaming) classes
+should not be necessary due to the ClassLoader isolation.
+
+[[ssh]]
+SSH Commands
+------------
+
+Plugins may provide commands that can be accessed through the SSH
+interface (extensions do not have this option).
+
+Command implementations must extend the base class SshCommand:
+
+====
+ import com.google.gerrit.sshd.SshCommand;
+
+ class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
+ }
+ }
+====
+
+If no Guice modules are declared in the manifest, SSH commands may
+use auto-registration by providing an `@Export` annotation:
+
+====
+ import com.google.gerrit.extensions.annotations.Export;
+ import com.google.gerrit.sshd.SshCommand;
+
+ @Export("print")
+ class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
+ }
+ }
+====
+
+If explicit registration is being used, a Guice module must be
+supplied to register the SSH command and declared in the manifest
+with the `Gerrit-SshModule` attribute:
+
+====
+ import com.google.gerrit.sshd.PluginCommandModule;
+
+ class MyCommands extends PluginCommandModule {
+ protected void configureCommands() {
+ command("print").to(PrintHello.class);
+ }
+ }
+====
+
+For a plugin installed as name `helloworld`, the command implemented
+by PrintHello class will be available to users as:
+
+----
+$ ssh -p 29418 review.example.com helloworld print
+----
+
+[[http]]
+HTTP Servlets
+-------------
+
+Plugins or extensions may register additional HTTP servlets, and
+wrap them with HTTP filters.
+
+Servlets may use auto-registration to declare the URL they handle:
+
+====
+ import com.google.gerrit.extensions.annotations.Export;
+ import com.google.inject.Singleton;
+ import javax.servlet.http.HttpServlet;
+ import javax.servlet.http.HttpServletRequest;
+ import javax.servlet.http.HttpServletResponse;
+
+ @Export("/print")
+ @Singleton
+ class HelloServlet extends HttpServlet {
+ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ res.setContentType("text/plain");
+ res.setCharacterEncoding("UTF-8");
+ res.getWriter().write("Hello");
+ }
+ }
+====
+
+The auto registration only works for standard servlet mappings like
+`/foo` or `/foo/*`. Regex style bindings must use a Guice ServletModule
+to register the HTTP servlets and declare it explicitly in the manifest
+with the `Gerrit-HttpModule` attribute:
+
+====
+ import com.google.inject.servlet.ServletModule;
+
+ class MyWebUrls extends ServletModule {
+ protected void configureServlets() {
+ serve("/print").with(HelloServlet.class);
+ }
+ }
+====
+
+For a plugin installed as name `helloworld`, the servlet implemented
+by HelloServlet class will be available to users as:
+
+----
+$ curl http://review.example.com/plugins/helloworld/print
+----
+
+[[data-directory]]
+Data Directory
+--------------
+
+Plugins can request a data directory with a `@PluginData` File
+dependency. A data directory will be created automatically by the
+server in `$site_path/data/$plugin_name` and passed to the plugin.
+
+Plugins can use this to store any data they want.
+
+====
+ @Inject
+ MyType(@PluginData java.io.File myDir) {
+ new FileInputStream(new File(myDir, "my.config"));
+ }
+====
+
+[[documentation]]
+Documentation
+-------------
+
+If a plugin does not register a filter or servlet to handle URLs
+`/Documentation/*` or `/static/*`, the core Gerrit server will
+automatically export these resources over HTTP from the plugin JAR.
+
+Static resources under `static/` directory in the JAR will be
+available as `/plugins/helloworld/static/resource`.
+
+Documentation files under `Documentation/` directory in the JAR
+will be available as `/plugins/helloworld/Documentation/resource`.
+
+Documentation may be written in
+link:http://daringfireball.net/projects/markdown/[Markdown] style
+if the file name ends with `.md`. Gerrit will automatically convert
+Markdown to HTML if accessed with extension `.html`.
+
+[[macros]]
+Within the Markdown documentation files macros can be used that allow
+to write documentation with reasonably accurate examples that adjust
+automatically based on the installation.
+
+The following macros are supported:
+
+[width="40%",options="header"]
+|===================================================
+|Macro | Replacement
+|@PLUGIN@ | name of the plugin
+|@URL@ | Gerrit Web URL
+|@SSH_HOST@ | SSH Host
+|@SSH_PORT@ | SSH Port
+|===================================================
+
+The macros will be replaced when the documentation files are rendered
+from Markdown to HTML.
+
+Macros that start with `\` such as `\@KEEP@` will render as `@KEEP@`
+even if there is an expansion for `KEEP` in the future.
+
+[[auto-index]]
+Automatic Index
+~~~~~~~~~~~~~~~
+
+If a plugin does not handle its `/` URL itself, Gerrit will
+redirect clients to the plugin's `/Documentation/index.html`.
+Requests for `/Documentation/` (bare directory) will also redirect
+to `/Documentation/index.html`.
+
+If neither resource `Documentation/index.html` or
+`Documentation/index.md` exists in the plugin JAR, Gerrit will
+automatically generate an index page for the plugin's documentation
+tree by scanning every `*.md` and `*.html` file in the Documentation/
+directory.
+
+For any discovered Markdown (`*.md`) file, Gerrit will parse the
+header of the file and extract the first level one title. This
+title text will be used as display text for a link to the HTML
+version of the page.
+
+For any discovered HTML (`*.html`) file, Gerrit will use the name
+of the file, minus the `*.html` extension, as the link text. Any
+hyphens in the file name will be replaced with spaces.
+
+If a discovered file name beings with `cmd-` it will be clustered
+into a 'Commands' section of the generated index page. All other
+files are clustered under a 'Documentation' section.
+
+Some optional information from the manifest is extracted and
+displayed as part of the index page, if present in the manifest:
+
+[width="40%",options="header"]
+|===================================================
+|Field | Source Attribute
+|Name | Implementation-Title
+|Vendor | Implementation-Vendor
+|Version | Implementation-Version
+|URL | Implementation-URL
+|API Version | Gerrit-ApiVersion
+|===================================================
+
+[[deployment]]
+Deployment
+----------
+
+Compiled plugins and extensions can be deployed to a running Gerrit
+server using the link:cmd-plugin-install.html[plugin install] command.
+
+Plugins can also be copied directly into the server's
+directory at `$site_path/plugins/$name.jar`. The name of
+the JAR file, minus the `.jar` extension, will be used as the
+plugin name. Unless disabled, servers periodically scan this
+directory for updated plugins. The time can be adjusted by
+link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
+
+For disabling plugins the link:cmd-plugin-remove.html[plugin remove]
+command can be used.
+
+Disabled plugins can be re-enabled using the
+link:cmd-plugin-enable.html[plugin enable] command.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
new file mode 100644
index 0000000000..799ff2dcd8
--- /dev/null
+++ b/Documentation/dev-release-subproject.txt
@@ -0,0 +1,99 @@
+Making a Release of a Gerrit Subproject / Core Plugin
+=====================================================
+
+Preparing a New Snapshot for Publishing
+---------------------------------------
+
+* You will need to have the following in the `pom.xml` to make it
+ deployable to the `gerrit-maven` storage bucket:
+
+----
+ <distributionManagement>
+ <repository>
+ <id>gerrit-maven</id>
+ <name>gerrit Maven Repository</name>
+ <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
+ <uniqueVersion>true</uniqueVersion>
+ </repository>
+ </distributionManagement>
+----
+
+
+* Add this to the `pom.xml` to enable the wagon provider:
+
+----
+ <build>
+ <extensions>
+ <extension>
+ <groupId>net.anzix.aws</groupId>
+ <artifactId>s3-maven-wagon</artifactId>
+ <version>3.2</version>
+ </extension>
+ </extensions>
+ </build>
+----
+
+
+* Add your username and password to your `~/.m2/settings.xml` file.
+ These need to come from the link:https://code.google.com/apis/console/[API Console].
+
+----
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
+ <servers>
+ <server>
+ <id>gerrit-maven</id>
+ <username>GOOG..EXAMPLE.....EXAMPLE</username>
+ <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
+ </server>
+ </servers>
+ </settings>
+----
+
+
+Making a Snapshot
+-----------------
+
+* Only for plugins: in the `pom.xml` update the Gerrit version under
+`properties` > `Gerrit-ApiVersion` to the version of the new Gerrit
+release
+* First build and deploy the latest snapshot and ensure that Gerrit
+builds/runs with this snapshot
+
+* Deploy the snapshot:
+
+====
+ mvn deploy
+====
+
+
+Making a Release
+----------------
+
+* First deploy (and test) the latest snapshot for the subproject/plugin
+
+* Update the top level `pom.xml` in the subproject/plugin to reflect
+the new project version (the exact value of the tag you will create
+below)
+
+* Commit the pom change and push to the project's repo
+`refs/for/<master/stable>`
+
+* Tag the version you just pushed (and push the tag)
+
+====
+ git tag -a -m "prolog-cafe 1.3" v1.3
+ git push gerrit-review refs/tags/v1.3:refs/tags/v1.3
+====
+
+* Deploy the new release:
+
+====
+ mvn deploy
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
new file mode 100644
index 0000000000..5ea3042090
--- /dev/null
+++ b/Documentation/dev-release.txt
@@ -0,0 +1,279 @@
+Making a Gerrit Release
+=======================
+
+[NOTE]
+========================================================================
+This document is meant primarily for Gerrit maintainers
+who have been given approval and submit status to the Gerrit
+projects. Additionally, maintainers should be given owner
+status to the Gerrit web site.
+========================================================================
+
+To make a Gerrit release involves a great deal of complex
+tasks and it is easy to miss a step so this document should
+hopefuly serve as both a how to for those new to the process
+and as a checklist for those already familiar with these
+tasks.
+
+
+Gerrit Release Type
+-------------------
+
+Here are some guidelines on release approaches depending on the
+type of release you want to make (stable-fix, stable, RC0, RC1...).
+
+Stable
+~~~~~~
+
+A stable release is generally built from the master branch and may need to
+undergo some stabilization before releasing the final release.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* Create a Gerrit RC0
+
+* If needed create a Gerrit RC1
+
+[NOTE]
+========================================================================
+You may let in a few features to this release
+========================================================================
+
+* If needed create a Gerrit RC2
+
+[NOTE]
+========================================================================
+There should be no new features in this release, only bug fixes
+========================================================================
+
+* Finally create the stable release (no RC)
+
+
+Stable-Fix
+~~~~~~~~~~
+
+Stable-fix releases should likely only contain bug fixes and doc updates.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* This type of release does not need any RCs, release when the objectives
+ are met
+
+
+
+Create the Actual Release
+---------------------------
+
+In the example commands below we assume that the last release was '2.4' and that
+we are preparing '2.5' release.
+
+Prepare the Subprojects
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* Publish the latest snapshot for all subprojects
+* Freeze all subprojects and link:dev-release-subproject.html[publish]
+ them!
+
+
+Prepare Gerrit
+~~~~~~~~~~~~~~
+
+* Create a `stable-2.5` branch for making the new release
+
+* In the `master` branch: Update the poms for the Gerrit version, push for
+review, get merged
+
+====
+ tools/version.sh --snapshot=2.5
+====
+
+* Checkout the `stable-2.5` branch
+* Update the top level `pom.xml` in Gerrit to ensure that none of the
+Subprojects point to snapshot releases
+
+* Tag
+
+====
+ git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
+ git tag -a -m "gerrit 2.5" v2.5
+====
+
+* Build (without plugins)
+
+====
+ ./tools/release.sh
+====
+
+[[plugin-api]]
+Publish the Plugin API JAR File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* Push JAR to `commondatastorage.googleapis.com`
+** Run `tools/deploy_api.sh`
+
+Prepare the Core Plugins
+~~~~~~~~~~~~~~~~~~~~~~~~
+* link:dev-release-subproject.html[Release and publish] the core plugins
+
+Package Gerrit with Plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* Ensure that the core plugins listed in `gerrit-package-plugins/pom.xml`
+point to the latest release version (no dependency to snapshot versions)
+* Include core plugins into WAR
+====
+ $ ./tools/version.sh --release && mvn clean package -f gerrit-package-plugins/pom.xml
+ $ ./tools/version.sh --reset
+====
+
+* Find WAR that includes the core plugins at
+`gerrit-package-plugins\target\gerrit-full-v2.5.war`
+* Sanity check WAR
+
+Publish to the Project Locations
+--------------------------------
+
+WAR File
+~~~~~~~~
+
+* Upload WAR to code.google.com/p/gerrit (manual web browser)
+** Go to http://code.google.com/p/gerrit/downloads/list
+** Use the "New Download" button
+
+* Update labels:
+** new war: [release-candidate], featured...
+** old war: deprecated
+
+Tag
+~~~
+
+* Push the New Tag
+
+====
+ git push gerrit-review refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+ git push gerrit-review refs/tags/v2.5:refs/tags/v2.5
+====
+
+
+Docs
+~~~~
+
+====
+ make -C Documentation PRIOR=2.4 update
+ make -C ReleaseNotes update
+====
+
+(no +PRIOR=+... if updating the same release again during RCs)
+
+* Update Google Code project links
+** Go to http://code.google.com/p/gerrit/admin
+** Point the main page to the new docs. The link to the documentation has to be
+updated at two places: in the project description and also in the Links
+section.
+** Point the main page to the new release notes
+
+[NOTE]
+========================================================================
+The docs makefile does an svn cp of the prior revision of the docs to branch
+the docs so you have less to upload on the new docs.
+
+User and password from here:
+
+ https://code.google.com/hosting/settings
+
+If subversion assumes a different username than your google one and asks for a
+password right away simply hit enter. Subversion will fail and then ask for
+another username and password. This time enter the username and password from
+the page linked above. After that subversion should save the username/password
+somewhere under `~/.subversion/auth` folder.
+========================================================================
+
+
+Issues
+~~~~~~
+
+====
+ How do the issues get updated? Do you run a script to do
+ this? When do you do it, after the final 2.2.2 is released?
+====
+
+By hand.
+
+Our current process is an issue should be updated to say Status =
+Submitted, FixedIn-2.2.2 once the change is submitted, but before the
+release.
+
+After the release is actually made, you can search in Google Code for
+``Status=Submitted FixedIn=2.2.2'' and then batch update these changes
+to say Status=Released. Make sure the pulldown says ``All Issues''
+because Status=Submitted is considered a closed issue.
+
+
+Mailing List
+~~~~~~~~~~~~
+
+* Send an email to the mailing list to announce the release, consider including some or all of the following in the email:
+** A link to the release and the release notes (if a final release)
+** A link to the docs
+** Describe the type of release (stable, bug fix, RC)
+
+----
+To: Repo and Gerrit Discussion <repo-discuss@googlegroups.com>
+Subject: Announce: Gerrit 2.2.2.1 (Stable bug fix update)
+
+I am pleased to announce Gerrit Code Review 2.2.2.1.
+
+Download:
+
+ http://code.google.com/p/gerrit/downloads/list
+
+
+This release is a stable bug fix release with some
+documentation updates including a new "Contributing to
+Gerrit" doc:
+
+ http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.2/dev-contributing.html
+
+
+To read more about the bug fixes:
+
+ http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.1.html
+
+-Martin
+----
+
+* Add an entry to the NEWS section of the main Gerrit project web page
+** Go to: http://code.google.com/p/gerrit/admin
+** Add entry like:
+----
+ * Jun 14, 2012 - Gerrit 2.4.1 [https://groups.google.com/d/topic/repo-discuss/jHg43gixqzs/discussion Released]
+----
+
+* Update the new discussion group announcement to be sticky
+** Go to: http://groups.google.com/group/repo-discuss/topics
+** Click on the announcement thread
+** Near the top right, click on options
+** Under options, cick the "Display this top first" checkbox
+** and Save
+
+* Update the previous discussion group announcement to no longer be sticky
+** See above (unclick checkbox)
+
+
+Merging Stable Fixes to master
+------------------------------
+
+After every stable-fix release, stable should be merged to master to
+ensure that none of the fixes ever get lost.
+
+====
+ git config merge.summary true
+ git checkout master
+ git reset --hard origin/master
+ git branch -f stable origin/stable
+ git merge stable
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/error-branch-not-found.txt b/Documentation/error-branch-not-found.txt
index e2dcff14b5..2aad0e1039 100644
--- a/Documentation/error-branch-not-found.txt
+++ b/Documentation/error-branch-not-found.txt
@@ -7,8 +7,8 @@ review if the specified target branch does not exist.
To push a change for code review the commit has to be pushed to the
project's magical `refs/for/'branch'` ref (for details have a look at
link:user-upload.html#push_create[Create Changes]).
-If you specify a non existing branch in the `refs/for/'branch'` ref
-the push is failing with the error message 'branch ... not found'.
+If you specify a non-existing branch in the `refs/for/'branch'` ref
+the push fails with the error message 'branch ... not found'.
To fix this problem verify
diff --git a/Documentation/error-change-closed.txt b/Documentation/error-change-closed.txt
index 7170a65917..3244fb35e2 100644
--- a/Documentation/error-change-closed.txt
+++ b/Documentation/error-change-closed.txt
@@ -1,8 +1,11 @@
change ... closed
=================
-With this error message Gerrit rejects to push a commit to a change
-that is already closed.
+With this error message Gerrit rejects to push a commit or submit a
+review label (approval) to a change that is already closed.
+
+When Pushing a Commit
+---------------------
This error occurs if you are trying to push a commit that contains
the Change-Id of a closed change in its commit message. A change can
@@ -14,7 +17,7 @@ already submitted and merged you may want to push your commit as a
new change. To do this you have to remove the Change-Id from the
commit message as explained link:error-push-fails-due-to-commit-message.html[here] and ideally generate a new Change-Id
using the link:cmd-hook-commit-msg.html[commit hook] or EGit. Before pushing again it is also
-recommendable to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
+recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
change. Pushing again should now create a new change in Gerrit.
If the change for which you wanted to upload a new patch set was
@@ -24,6 +27,14 @@ this change you may want to restore the change in the Gerrit WebUI
'Restore Change' button). Afterwards the push should succeed and a
new patch set for this change will be created.
+When Submitting a Review Label
+------------------------------
+
+This error occurs if you are trying to submit a review label (approval) using
+the link:cmd-review.html[ssh review command] after the change has been closed.
+A change can be closed because it was submitted and merged, because it was abandoned,
+or because the patchset to which you are submitting the review has been replaced
+by a newer patchset.
GERRIT
------
diff --git a/Documentation/error-change-does-not-belong-to-project.txt b/Documentation/error-change-does-not-belong-to-project.txt
index 29957e1312..e747881f48 100644
--- a/Documentation/error-change-does-not-belong-to-project.txt
+++ b/Documentation/error-change-does-not-belong-to-project.txt
@@ -7,7 +7,7 @@ that belongs to another project.
This error message means that the user explicitly pushed a commit to
a change that belongs to another project by specifying it as target
ref. This way of adding a new patch set to a change is deprecated as
-explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-ID's for
+explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-IDs for
link:user-upload.html#push_replace[replacing changes].
diff --git a/Documentation/error-change-not-found.txt b/Documentation/error-change-not-found.txt
index c9ac0d8c5a..b6df13b796 100644
--- a/Documentation/error-change-not-found.txt
+++ b/Documentation/error-change-not-found.txt
@@ -7,7 +7,7 @@ that cannot be found.
This error message means that the user explicitly pushed a commit to
a non-existing change by specifying it as target ref. This way of
adding a new patch set to a change is deprecated as explained link:user-upload.html#manual_replacement_mapping[here].
-It is recommended to only rely on Change-ID's for link:user-upload.html#push_replace[replacing changes].
+It is recommended to only rely on Change-IDs for link:user-upload.html#push_replace[replacing changes].
GERRIT
diff --git a/Documentation/error-you-are-not-author.txt b/Documentation/error-invalid-author.txt
index a2452527b0..c484776a78 100644
--- a/Documentation/error-you-are-not-author.txt
+++ b/Documentation/error-invalid-author.txt
@@ -1,10 +1,10 @@
-you are not author ...
-======================
+invalid author
+==============
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
the author matches one of the registered e-mail addresses of the
pushing user. If this is not the case pushing the commit fails with
-the error message "you are not author ...". This policy can be
+the error message "invalid author". This policy can be
bypassed by having the access right
link:access-control.html#category_forge_author['Forge Author'].
@@ -17,8 +17,8 @@ This error may happen for two reasons:
Incorrect configuration of the e-mail address on client or server side
----------------------------------------------------------------------
-If pushing to Gerrit fails with the error message "you are not
-author ..." and you are the author of the commit for which the push
+If pushing to Gerrit fails with the error message "invalid author"
+and you are the author of the commit for which the push
fails, then either you have not successfully registered this e-mail
address for your Gerrit account or the author information of the
pushed commit is incorrect.
@@ -27,7 +27,7 @@ Configuration of e-mail address in Gerrit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account. If no e-mail address is
registered go to 'Settings -> Contact Information' and register a new
e-mail address there. Make sure you confirm your e-mail address by
clicking on the link in the e-mail verification mail sent by Gerrit.
@@ -92,7 +92,7 @@ gets more complicated. In this case you have to do an interactive
git rebase for the affected commits. While doing the interactive
rebase you have to choose 'edit' for those commits for which the
author should be rewritten. When the rebase stops at such a commit
-you have to amend the commit with explicitly setting the author
+you have to amend the commit, explicitly setting the author
before continuing the rebase.
Here is an example that shows how the interactive rebase is used to
@@ -131,8 +131,8 @@ link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documen
Missing privileges to push commits of other users
-------------------------------------------------
-If pushing to Gerrit fails with the error message "you are not
-author ..." and somebody else is author of the commit for which the
+If pushing to Gerrit fails with the error message "invalid author"
+and somebody else is author of the commit for which the
push fails, then you have no permissions to forge the author
identity. In this case you may contact the project owner to request
the access right '+1 Forge Author Identity' in the 'Forge Identity'
diff --git a/Documentation/error-you-are-not-committer.txt b/Documentation/error-invalid-committer.txt
index b5b8c44aed..447064efdb 100644
--- a/Documentation/error-you-are-not-committer.txt
+++ b/Documentation/error-invalid-committer.txt
@@ -1,10 +1,10 @@
-you are not committer ...
-=========================
+invalid committer
+=================
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
the committer matches one of the registered e-mail addresses of the
pushing user. If this is not the case pushing the commit fails with
-the error message "you are not committer ...". This policy can be
+the error message "invalid committer". This policy can be
bypassed by having the access right
link:access-control.html#category_forge_committer['Forge Committer'].
@@ -19,8 +19,8 @@ This error may happen for two reasons:
Incorrect configuration of the e-mail address on client or server side
----------------------------------------------------------------------
-If pushing to Gerrit fails with the error message "you are not
-committer ..." and you committed the change for which the push fails,
+If pushing to Gerrit fails with the error message "invalid committer"
+and you committed the change for which the push fails,
then either you have not successfully registered this e-mail address
for your Gerrit account or the committer information of the pushed
commit is incorrect.
@@ -29,7 +29,7 @@ Configuration of e-mail address in Gerrit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account. If no e-mail address is
registered go to 'Settings -> Contact Information' and register a new
e-mail address there. Make sure you confirm your e-mail address by
clicking on the link in the e-mail verification mail sent by Gerrit.
@@ -96,8 +96,8 @@ link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git documen
Missing privileges to push commits that were committed by other users
---------------------------------------------------------------------
-If pushing to Gerrit fails with the error message "you are not
-committer ..." and somebody else committed the change for which the
+If pushing to Gerrit fails with the error message "invalid committer"
+and somebody else committed the change for which the
push fails, then you have no permissions to forge the committer
identity. In this case you may contact the project owner to request
the access right '+2 Forge Committer or Tagger Identity' in the
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index f915a14b13..c9df8835e1 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -15,7 +15,9 @@ Error Messages
* link:error-change-not-found.html[change ... not found]
* link:error-contains-banned-commit.html[contains banned commit ...]
* link:error-has-duplicates.html[... has duplicates]
+* link:error-invalid-author.html[invalid author]
* link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message]
+* link:error-invalid-committer.html[invalid committer]
* link:error-missing-changeid.html[missing Change-Id in commit message]
* link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message]
* link:error-no-changes-made.html[no changes made]
@@ -33,8 +35,6 @@ Error Messages
* link:error-squash-commits-first.html[squash commits first]
* link:error-upload-denied.html[Upload denied for project \'...']
* link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
-* link:error-you-are-not-author.html[you are not author ...]
-* link:error-you-are-not-committer.html[you are not committer ...]
General Hints
diff --git a/Documentation/error-no-changes-made.txt b/Documentation/error-no-changes-made.txt
index 7ef708271f..d0e1d4f2a9 100644
--- a/Documentation/error-no-changes-made.txt
+++ b/Documentation/error-no-changes-made.txt
@@ -2,10 +2,10 @@ no changes made
===============
With this error message Gerrit rejects to push a commit as a new
-patch set for a change, if the pushed commit is identical with the
+patch set for a change, if the pushed commit is identical to the
current patch set of this change.
-A pushed commit is considered to be identical with the current patch
+A pushed commit is considered to be identical to the current patch
set if
- the files in the commit,
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 347c080f23..8e409efa55 100644
--- a/Documentation/error-no-new-changes.txt
+++ b/Documentation/error-no-new-changes.txt
@@ -3,7 +3,7 @@ no new changes
With this error message Gerrit rejects to push a commit if the pushed
commit was already successfully pushed to Gerrit. In this case there
-is no new change and consequently there is nothing to do for Gerrit.
+is no new change and consequently there is nothing for Gerrit to do.
If your push is failing with this error message, you normally
don't have to do anything since the commit was already successfully
diff --git a/Documentation/error-non-fast-forward.txt b/Documentation/error-non-fast-forward.txt
index 7dba51bc27..6604e10606 100644
--- a/Documentation/error-non-fast-forward.txt
+++ b/Documentation/error-non-fast-forward.txt
@@ -1,15 +1,15 @@
non-fast forward
================
-With this error message Git rejects a push if the remote branch can't
+With this error message Gerrit rejects a push if the remote branch can't
be fast forwarded onto the pushed commit. This is the case if the
pushed commit is not based on the current tip of the remote branch.
If a non-fast forward update would be done, all commits from the
remote branch that succeed the base commit of the pushed commit would
be removed. This would be especially confusing for other users that
-have based their work on such a commit. Because of this Git is by
-default not allowing non-fast forward updates.
+have based their work on such a commit. Because of this Git by
+default does not allow non-fast forward updates.
When working with Gerrit, this error can only occur if
link:user-upload.html#bypass_review[code review is bypassed].
@@ -46,7 +46,7 @@ should check the push specification and verify that you are pushing
the commit to the correct project.
-Although it is considered as bad practice, it is possible to allow
+Although it is considered bad practice, it is possible to allow
non-fast forward updates with Git. For this the remote Git repository
has to be configured to not deny non-fast forward updates (set the
link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[Git configuration] parameter 'receive.denyNonFastForwards' to
diff --git a/Documentation/error-not-a-gerrit-administrator.txt b/Documentation/error-not-a-gerrit-administrator.txt
index 0468d830aa..b771af69d1 100644
--- a/Documentation/error-not-a-gerrit-administrator.txt
+++ b/Documentation/error-not-a-gerrit-administrator.txt
@@ -1,7 +1,7 @@
Not a Gerrit administrator
==========================
-With this error message Gerrit rejects to execute a SSH command that
+With this error message Gerrit rejects to execute an SSH command that
requires administrator privileges if the user is not a Gerrit
administrator.
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-not-a-gerrit-project.txt
index 368a102a9a..dac98ae666 100644
--- a/Documentation/error-not-a-gerrit-project.txt
+++ b/Documentation/error-not-a-gerrit-project.txt
@@ -18,7 +18,7 @@ If you are facing this problem, do the following:
project is listed. If the project is not listed the project either
does not exist or you don't have
link:access-control.html#category_read['Read'] access for it. This
- means if you certain that the project name is right you should
+ means if you are certain that the project name is right you should
contact the Gerrit Administrator or project owner to request access
to the project.
diff --git a/Documentation/error-not-allowed-to-upload-merges.txt b/Documentation/error-not-allowed-to-upload-merges.txt
index 981ba91ccc..515eef5ccf 100644
--- a/Documentation/error-not-allowed-to-upload-merges.txt
+++ b/Documentation/error-not-allowed-to-upload-merges.txt
@@ -2,11 +2,11 @@ you are not allowed to upload merges
====================================
With this error message Gerrit rejects to push a merge commit if the
-pushing user has no permissions to upload merge commits for the
+pushing user has no permission to upload merge commits for the
project to which the push is done.
If you need to upload merge commits, you can contact one of the
-project owners and request permissions to upload merge commits
+project owners and request permission to upload merge commits
(access right link:access-control.html#category_push_merge['Push Merge Commit'])
for this project.
diff --git a/Documentation/error-permission-denied.txt b/Documentation/error-permission-denied.txt
index 1cb57089cc..2ec0a3ff10 100644
--- a/Documentation/error-permission-denied.txt
+++ b/Documentation/error-permission-denied.txt
@@ -1,7 +1,7 @@
Permission denied (publickey)
=============================
-With this error message a SSH command to Gerrit is rejected if the
+With this error message an SSH command to Gerrit is rejected if the
SSH authentication is not successful.
The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol uses link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography] for authentication.
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 69f80c17a6..bad2b3c060 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -17,10 +17,17 @@ In particular this error occurs:
3. if you push an annotated tag without
link:access-control.html#category_push_annotated['Push Annotated Tag']
access right on 'refs/tags/*'
-4. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
+4. if you push a signed tag without
+ link:access-control.html#category_push_signed['Push Signed Tag']
+ access right on 'refs/tags/*'
+5. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
Reference'] for the reference name 'refs/tags/*'
+6. if you push a tag with somebody else as tagger and you don't have the
+ link:access-control.html#category_forge_committer['Forge Committer']
+ access right for the reference name 'refs/tags/*'
+7. if you push to a project that is in state 'Read Only'
-For new users it happens often that they accidentally try to bypass
+For new users it often happens that they accidentally try to bypass
code review. The push then fails with the error message 'prohibited
by Gerrit' because the project didn't allow to bypass code review.
Bypassing the code review is done by pushing directly to refs/heads/*
diff --git a/Documentation/error-push-fails-due-to-commit-message.txt b/Documentation/error-push-fails-due-to-commit-message.txt
index 01e0a8ea46..172d64f6d6 100644
--- a/Documentation/error-push-fails-due-to-commit-message.txt
+++ b/Documentation/error-push-fails-due-to-commit-message.txt
@@ -3,7 +3,7 @@ Push fails due to commit message
If Gerrit rejects pushing a commit it is often the case that there is
an issue with the commit message of the pushed commit. In this case
-often the problem can be resolved by fixing the commit message.
+the problem can often be resolved by fixing the commit message.
If the commit message of the last commit needs to be fixed you can
simply amend the last commit (please find a detailed description in
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-squash-commits-first.txt
index 138ad98951..2181c52985 100644
--- a/Documentation/error-squash-commits-first.txt
+++ b/Documentation/error-squash-commits-first.txt
@@ -9,7 +9,7 @@ the corresponding change in Gerrit, a dependency upon itself. Gerrit
prevents such dependencies between patch sets within the same change
to keep the review process simple. Otherwise reviewers would not only
have to review the latest patch set but also all the patch sets the
-latest one is depending on.
+latest one depends on.
This error is quite common, it appears when a user tries to address
review comments and creates a new commit instead of amending the
@@ -93,8 +93,8 @@ the link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[Git doc
----
If it was the intention to create a patch series with multiple
-changes to be reviewed each commit message should contain the
-Change-ID of the corresponding change in Gerrit, if a change in
+changes to be reviewed, each commit message should contain the
+Change-ID of the corresponding change in Gerrit. If a change in
Gerrit does not exist yet, the Change-ID should be generated (either
by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-ID could be
removed (not recommended since then amending this commit to create
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
index 080ecb672f..a84c3dce6b 100644
--- a/Documentation/i18n-readme.txt
+++ b/Documentation/i18n-readme.txt
@@ -1,7 +1,7 @@
Gerrit Code Review - i18n
=========================
-Aside from actually writing translations, there's some issues with
+Aside from actually writing translations, there are some issues with
the way the code produces output. Most of the UI should support
right-to-left (RTL) languages.
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 5143bf762d..4c2335f699 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -18,7 +18,13 @@ User Guide
* link:user-signedoffby.html[Signed-off-by Lines]
* link:access-control.html[Access Controls]
* link:error-messages.html[Error Messages]
+* link:rest-api.html[REST API]
+* link:user-custom-dashboards.html[Custom Dashboards]
+* link:user-notify.html[Subscribing to Email Notifications]
* link:user-submodules.html[Subscribing to Git Submodules]
+* link:refs-notes-review.html[The `refs/notes/review` namespace]
+* link:prolog-cookbook.html[Prolog Cookbook]
+* link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
Installation
------------
@@ -33,7 +39,6 @@ Configuration
* link:config-gerrit.html[System Settings]
* link:config-contact.html[User Contact Information]
-* link:config-replication.html[Git Replication/Mirroring]
* link:config-gitweb.html[Gitweb Integration]
* link:config-headerfooter.html[Site Header/Footer]
* link:config-sso.html[Single Sign-On Systems]
@@ -47,8 +52,11 @@ Developer Documentation
* link:dev-readme.html[Developer Setup]
* link:dev-eclipse.html[Eclipse Setup]
* link:dev-contributing.html[Contributing to Gerrit]
+* link:dev-plugins.html[Developing Plugins]
* link:dev-design.html[System Design]
* link:i18n-readme.html[i18n Support]
+* link:dev-release.html[Developer Release]
+* link:dev-release-subproject.html[Developer Subproject Release]
Resources
---------
@@ -56,5 +64,5 @@ Resources
* link:http://code.google.com/p/gerrit/[Homepage]
* link:http://code.google.com/p/gerrit/downloads/list[Downloads]
* link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source Code]
+* link:http://code.google.com/p/gerrit/source/checkout[Source Code]
* link:http://code.google.com/p/gerrit/wiki/Background[A History of Gerrit Code Review]
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 507d6c5a26..96814a0dbc 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -44,7 +44,7 @@ is configured and enabled within the DataSource.
+
If you enabled Bouncy Castle Crypto during 'init', copy the JAR
from `'$site_path'/lib` into your servlet container's extensions
-directory so its available to Gerrit Code Review.
+directory so it's available to Gerrit Code Review.
Jetty 7.x
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 6bea7f8db2..c09c197f7f 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -12,7 +12,7 @@ It is presumed you install it on a Unix based server such as any of the Linux
flavors or BSD.
It's also presumed that you have access to an OpenID enabled email address.
-Examples of OpenID enable email providers are gmail, yahoo and hotmail.
+Examples of OpenID enable email providers are Gmail, Yahoo! Mail and Hotmail.
It's also possible to register a custom email address with OpenID, but that is
outside the scope of this quick installation guide. For testing purposes one of
the above providers should be fine. Please note that network access to the
@@ -42,7 +42,7 @@ If Java isn't installed, get it:
Create a user to host the Gerrit service
----------------------------------------
-We will run the service as a non privileged user on your system.
+We will run the service as a non-privileged user on your system.
First create the user and then become the user:
----
@@ -50,7 +50,7 @@ First create the user and then become the user:
$ sudo su gerrit2
----
-If you don't have root privileges you could skip this step and run gerrit
+If you don't have root privileges you could skip this step and run Gerrit
as your own user as well.
@@ -58,7 +58,7 @@ as your own user as well.
Download Gerrit
---------------
-It's time to download the archive that contains the gerrit web and ssh service.
+It's time to download the archive that contains the Gerrit web and ssh service.
You can choose from different versions to download from here:
@@ -87,14 +87,28 @@ It's time to run the initialization, and with the batch switch enabled, we don't
When the init is complete, you can review your settings in the
file `'$site_path/etc/gerrit.config'`.
-An important setting will be the canonicalWebUrl which will
-be needed later to access gerrit's web interface.
+Note that initialization also starts the server. If any settings changes are
+made, the server must be restarted before they will take effect.
----
- gerrit2@host:~$ cat ~/gerrit_testsite/etc/gerrit.config | grep canonical
- canonicalWebUrl = http://localhost:8080/
+ gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh restart
+ Stopping Gerrit Code Review: OK
+ Starting Gerrit Code Review: OK
+ gerrit2@host:~$
+----
+
+The server can be also stopped and started by passing the `stop` and `start`
+commands to gerrit.sh.
+
+----
+ gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh stop
+ Stopping Gerrit Code Review: OK
+ gerrit2@host:~$
+ gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh start
+ Starting Gerrit Code Review: OK
gerrit2@host:~$
----
+
[[usersetup]]
The first user
--------------
@@ -154,15 +168,32 @@ SSH key generation
Registering your key in Gerrit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Open a browser and enter the canonical url you got before when
-initializing Gerrit.
+Open a browser and enter the canonical url of your Gerrit server. You can
+find the url in the settings file.
----
- Canonical URL [http://localhost:8080/]:
+ gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config gerrit.canonicalWebUrl
+ http://localhost:8080/
+ gerrit2@host:~$
----
Register a new account in Gerrit through the web interface with the
email address of your choice.
+
+The default authentication type is OpenID. If your Gerrit server is behind a
+proxy, and you are using an external OpenID provider, you will need to add the
+proxy settings in the configuration file.
+
+----
+ gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxy http://proxy:8080
+ gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyUsername username
+ gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyPassword password
+----
+
+Refer to the Gerrit configuration guide for more detailed information about
+link:config-gerrit.html#auth[authentication] and
+link:config-gerrit.html#http.proxy[proxy] settings.
+
The first user to sign-in and register an account will be
automatically placed into the fully privileged Administrators group,
permitting server management over the web and over SSH. Subsequent
@@ -216,7 +247,7 @@ Project creation
Your base Gerrit server is now running and you have a user that's ready
to interact with it. You now have two options, either you create a new
test project to work with or you already have a git with history that
-you would like to import into gerrit and try out code review on.
+you would like to import into Gerrit and try out code review on.
New project from scratch
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -231,14 +262,14 @@ This is done via the SSH port:
user@host:~$
----
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
Already existing project
~~~~~~~~~~~~~~~~~~~~~~~~
The other alternative is if you already have a git project that you
want to try out Gerrit on.
-First you have to create the project, this is done via the SSH port:
+First you have to create the project. This is done via the SSH port:
----
user@host:~$ ssh -p 29418 user@localhost gerrit create-project --name demo-project
@@ -262,7 +293,7 @@ After that it's time to upload the previous history to the server:
user@host:~/my-project$
----
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
My first change
@@ -294,7 +325,7 @@ Then make a change to it and upload it as a reviewable change in Gerrit.
Usually when you push to a remote git, you push to the reference
`'/refs/heads/branch'`, but when working with Gerrit you have to push to a
-virtual branch representing "code review before submittal to branch".
+virtual branch representing "code review before submission to branch".
This virtual name space is known as /refs/for/<branch>
----
@@ -319,11 +350,11 @@ Quick Installation Complete
---------------------------
This covers the scope of getting Gerrit started and your first change uploaded.
-It doesn't give any clue as to how the review workflow works, please find
+It doesn't give any clue as to how the review workflow works, please read
link:http://source.android.com/submit-patches/workflow[Default Workflow] to
learn more about the workflow of Gerrit.
-To read more on the installation of Gerrit please read link:install.html[the detailed
+To read more on the installation of Gerrit please see link:install.html[the detailed
installation page].
diff --git a/Documentation/install.txt b/Documentation/install.txt
index b90bbcef7a..9926b8fd0c 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -3,7 +3,7 @@ Gerrit Code Review - Installation Guide
[[requirements]]
Requirements
------------
+------------
To run the Gerrit service, the following requirements must be met on
the host:
@@ -199,11 +199,10 @@ Site Customization
------------------
Gerrit Code Review supports some site-specific customization options.
-For more information, see the related topic in this manual:
+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-replication.html[Git Replication/Mirroring]
* link:config-headerfooter.html[Site Header/Footer]
* link:config-gitweb.html[Gitweb Integration]
* link:config-gerrit.html[Other System Settings]
@@ -222,6 +221,13 @@ for details on how to configure this if anonymous access is desired.
* http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[man git-daemon]
+[[plugins]]
+Plugins
+-------
+
+Place Gerrit plugins in the review_site/plugins directory to have them loaded on Gerrit startup.
+
+
External Documentation Links
----------------------------
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 3d5cbcbb00..25f5d5e44a 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -27,7 +27,7 @@ applied to the code base. However Gerrit goes a step further making it
simple for all committers on a project to ensure that changes are
checked over before they're actually applied. Because of this Gerrit
is equally useful where all users are trusted committers such as may
-the case with closed-source commercial development. Either way it's
+be the case with closed-source commercial development. Either way it's
still desirable to have code reviewed to improve the quality and
maintainability of the code. After all, if only one person has seen
the code it may be a little difficult to maintain when that person
@@ -337,7 +337,7 @@ HEAD is now at d5dacdb... Change to a proper, yeast based pizza dough.
Easy as that, we now have the change in our working copy to play with.
You might be interested in what the numbers of the refspec mean.
-* The first *68* is the id if the change +mod 100+. The only reason
+* The first *68* is the id of the change +mod 100+. The only reason
for this initial number is to reduce the number of files in any given
directory within the git repository.
* The second *68* is the full id of the change. You'll notice this in
@@ -379,7 +379,7 @@ main screen. Just as Code Review and Verify are different operations
that can be done by different users, Submission is a third operation
that can be limited down to another group of users.
-Activating the _Publish and Submit_ or _Submit Patch Set X_ button
+Clicking the _Publish and Submit_ or _Submit Patch Set X_ button
will merge the change into the main part of the repository so that it
becomes an accepted part of the project. After this anyone fetching
the git repository will receive this change as a part of the master
diff --git a/Documentation/json.txt b/Documentation/json.txt
index b1dbc32cc9..f0588ceb17 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -11,22 +11,22 @@ change
------
The Gerrit change being reviewed, or that was already reviewed.
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
-branch:: Branch name within project
+branch:: Branch name within project.
-topic:: Topic name specified by the uploader for this change series
+topic:: Topic name specified by the uploader for this change series.
id:: Change identifier, as scraped out of the Change-Id field in
the commit message, or as assigned by the server if it was missing.
-number:: Change number (deprecated)
+number:: Change number (deprecated).
-subject:: Description of change
+subject:: Description of change.
-owner:: Owner in <<account,account attribute>>
+owner:: Owner in <<account,account attribute>>.
-url:: Canonical URL to reach this change
+url:: Canonical URL to reach this change.
commitMessage:: The full commit message for the change.
@@ -53,9 +53,12 @@ trackingIds:: Issue tracking system links in
message based on the server's
link:config-gerrit.html#trackingid[trackingid] sections.
-currentPatchSet:: Current <<patchset,patchset attribute>>.
+currentPatchSet:: Current <<patchSet,patchSet attribute>>.
-patchSets:: All <<patchset,patchset attribute>> for this change.
+patchSets:: All <<patchSet,patchSet attribute>> for this change.
+
+submitRecords:: The <<submitRecord,submitRecord attribute>> contains
+information about whether this change has been or can be submitted.
[[trackingid]]
trackingid
@@ -76,8 +79,10 @@ name:: User's full name, if configured.
email:: User's preferred email address.
-[[patchset]]
-patchset
+username:: User's username, if configured.
+
+[[patchSet]]
+patchSet
--------
Refers to a specific patchset within a <<change,change>>.
@@ -109,8 +114,8 @@ was added or last updated.
by:: Reviewer of the patch set in <<account,account attribute>>.
-[[refupdate]]
-refupdate
+[[refUpdate]]
+refUpdate
--------
Information about a ref that was updated.
@@ -118,10 +123,61 @@ oldRev:: The old value of the ref, prior to the update.
newRev:: The new value the ref was updated to.
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
refName:: Ref name within project.
+[[queryLimit]]
+queryLimit
+----------
+Information about the link:access-control.html#capability_queryLimit[queryLimit]
+of a user.
+
+min:: lower limit
+
+max:: upper limit
+
+[[submitRecord]]
+submitRecord
+------------
+Information about the submit status of a change.
+
+status:: Current submit status.
+
+ OK;; The change is ready for submission or already submitted.
+
+ NOT_READY;; The change is missing a required label.
+
+ RULE_ERROR;; An internal server error occurred preventing computation.
+
+labels:: This describes the state of each code review
+<<label,label attribute>>, unless the status is RULE_ERROR.
+
+[[label]]
+label
+-----
+Information about a code review label for a change.
+
+label:: The name of the label.
+
+status:: The status of the label.
+
+ OK;; This label provides what is necessary for submission.
+
+ REJECT;; This label prevents the change from being submitted.
+
+ NEED;; The label is required for submission, but has not
+ been satisfied.
+
+ MAY;; The label may be set, but it's neither necessary for
+ submission nor does it block submission if set.
+
+ IMPOSSIBLE;; The label is required for submission, but is impossible
+ to complete. The likely cause is access has not been granted
+ correctly by the project owner or site administrator.
+
+by:: The <<account,account>> that applied the label.
+
SEE ALSO
--------
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69018d8786..7787fe8757 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -18,6 +18,7 @@ Included Components
|Google Gson | <<apache2,Apache License 2.0>>
|Google Web Toolkit | <<apache2,Apache License 2.0>>
|Guice | <<apache2,Apache License 2.0>>
+|Guava Libraries | <<apache2,Apache License 2.0>>
|Apache Commons Codec | <<apache2,Apache License 2.0>>
|Apache Commons DBCP | <<apache2,Apache License 2.0>>
|Apache Commons Http Client | <<apache2,Apache License 2.0>>
@@ -27,13 +28,12 @@ Included Components
|Apache Commons Pool | <<apache2,Apache License 2.0>>
|Apache Log4J | <<apache2,Apache License 2.0>>
|Apache MINA | <<apache2,Apache License 2.0>>
-|Apache Tomact Servlet API | <<apache2,Apache License 2.0>>
+|Apache Tomcat Servlet API | <<apache2,Apache License 2.0>>
|Apache SSHD | <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
|Apache Velocity | <<apache2,Apache License 2.0>>
|Apache Xerces | <<apache2,Apache License 2.0>>
|OpenID4Java | <<apache2,Apache License 2.0>>
|Neko HTML | <<apache2,Apache License 2.0>>
-|Ehcache | <<apache2,Apache License 2.0>>
|mime-util | <<apache2,Apache License 2.0>>
|Jetty | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
|Prolog Cafe | <<prolog_cafe,EPL or GPL>>
@@ -52,6 +52,7 @@ Included Components
|JSR 305 | <<jsr305,New-Style BSD>>
|dk.brics.automaton | <<automaton,New-Style BSD>>
|Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
+|pegdown | <<apache2,Apache License 2.0>>
|======================================================================
Cryptography Notice
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
index 17cc862eb6..1b00213905 100644
--- a/Documentation/pgm-ExportReviewNotes.txt
+++ b/Documentation/pgm-ExportReviewNotes.txt
@@ -3,7 +3,7 @@ ExportReviewNotes
NAME
----
-ExportReviewNotes - Export successful reviews to refs/notes/review
+ExportReviewNotes - Export successful reviews to link:refs-notes-review.html[refs/notes/review]
SYNOPSIS
--------
@@ -14,7 +14,7 @@ DESCRIPTION
-----------
Scans every submitted change and creates an initial notes
branch detailing the previous submission information for
-each merged changed.
+each merged change.
This task can take quite some time, but can run in the background
concurrently to the server if the database is MySQL or PostgreSQL.
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index c53c57dd29..57decdd8e6 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -19,7 +19,7 @@ Creates a new Gerrit server installation, interactively prompting
for some basic setup prior to writing default configuration files
into a newly created `$site_path`.
-If run an an existing `$site_path`, init will upgrade some resources
+If run in an existing `$site_path`, init will upgrade some resources
as necessary.
OPTIONS
@@ -28,6 +28,11 @@ OPTIONS
Run in batch mode, skipping interactive prompts. Reasonable
configuration defaults are chosen based on the whims of
the Gerrit developers.
++
+If during a schema migration unused objects (e.g. tables, columns)
+are detected they are *not* automatically dropped, but only a list of
+SQL statements to drop these objects is provided. To drop the unused
+objects these SQL statements have to be executed manually.
\--no-auto-start::
Don't automatically start the daemon after initializing a
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
new file mode 100644
index 0000000000..d5f6174c12
--- /dev/null
+++ b/Documentation/prolog-change-facts.txt
@@ -0,0 +1,100 @@
+Prolog Facts for Gerrit Changes
+===============================
+
+Prior to invoking the `submit_rule(X)` query for a change, Gerrit initializes
+the Prolog engine with a set of facts (current data) about this change.
+The following table provides an overview of the provided facts.
+
+IMPORTANT: All the terms listed below are defined in the `gerrit` package. To use any
+of them we must use a qualified name like `gerrit:change_branch(X)`.
+
+.Prolog facts about the current change
+[grid="cols"]
+[options="header"]
+|=============================================================================
+|Fact |Example |Description
+
+|`change_branch/1` |`change_branch('refs/heads/master').`
+ |Destination Branch for the change as string atom
+
+|`change_owner/1` |`change_owner(user(1000000)).`
+ |Owner of the change as `user(ID)` term. ID is the numeric account ID
+
+|`change_project/1` |`change_project('full/project/name').`
+ |Name of the project as string atom
+
+|`change_topic/1` |`change_topic('plugins').`
+ |Topic name as string atom
+
+|`commit_author/1` |`commit_author(user(100000)).`
+ |Author of the commit as `user(ID)` term. ID is the numeric account ID
+
+|`commit_author/3` |`commit_author(user(100000), 'John Doe', 'john.doe@example.com').`
+ |ID, full name and the email of the commit author. The full name and the
+ email are string atoms
+
+|`commit_committer/1` |`commit_committer()`
+ |Committer of the commit as `user(ID)` term. ID is the numeric account ID
+
+|`commit_committer/3` |`commit_committer()`
+ |ID, full name and the email of the commit committer. The full name and the
+ email are string atoms
+
+.2+|`commit_label/2` |`commit_label(label('Code-Review', 2), user(1000000)).`
+ .2+|Set of votes on the last patch-set
+
+ |`commit_label(label('Verified', -1), user(1000001)).`
+
+|`commit_message/1` |`commit_message('Fix bug X').`
+ |Commit message as string atom
+
+.4+|`current_user/1` |`current_user(user(1000000)).`
+ .4+|Current user as one of the four given possibilities
+
+ |`current_user(user(anonymous)).`
+ |`current_user(user(peer_daemon)).`
+ |`current_user(user(replication)).`
+|=============================================================================
+
+In addition Gerrit provides a set of built-in helper predicates that can be used
+when implementing the `submit_rule` predicate. The most common ones are listed in
+the following table.
+
+.Built-in Prolog helper predicates
+[grid="cols"]
+[options="header"]
+|=============================================================================
+|Predicate |Example usage |Description
+
+|`commit_delta/1` |`commit_delta('\\.java$').`
+ |True if any file name from the last patch set matches the given regex.
+
+|`commit_delta/3` |`commit_delta('\\.java$', T, P)`
+ |Returns the change type (via `T`) and path (via `P`), if the change type
+ is `rename`, it also returns the old path. If the change type is `rename`, it
+ returns a delete for old path and an add for new path. If the change type
+ is `copy`, an add is returned along with new path.
+
+ Possible values for the change type are the following symbols: `add`,
+ `modify`, `delete`, `rename`, `copy`
+
+|`commit_delta/4` |`commit_delta('\\.java$', T, P, O)`
+ |Like `commit_delta/3` plus the old path (via `O`) if applicable.
+
+|`commit_edits/2` |`commit_edits('/pom.xml$', 'dependency')`
+ |True if any of the files matched by the file name regex (first parameter)
+ have edited lines that match the regex in the second parameter. This
+ example will be true if there is a modification of a `pom.xml` file such
+ that an edited line contains or contained the string `'dependency'`.
+
+|`commit_message_matches/1` |`commit_message_matches('^Bug fix')`
+ |True if the commit message matches the given regex.
+
+|=============================================================================
+
+NOTE: for a complete list of built-in helpers read the `gerrit_common.pl` and
+all Java classes whose name matches `PRED_*.java` from Gerrit's source code.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
new file mode 100644
index 0000000000..e660067a87
--- /dev/null
+++ b/Documentation/prolog-cookbook.txt
@@ -0,0 +1,750 @@
+Gerrit Code Review - Prolog Submit Rules Cookbook
+=================================================
+
+Submit Rule
+-----------
+A 'Submit Rule' in Gerrit is logic that defines when a change is submittable.
+By default, a change is submittable when it gets at least one
+highest vote in each voting category and has no lowest vote (aka veto vote) in
+any category. Typically, this means that a change needs 'Code-Review+2',
+'Verified+1' and has neither 'Code-Review-2' nor 'Verified-1' to become
+submittable.
+
+While this rule is a good default, there are projects which need more
+flexibility for defining when a change is submittable. In Gerrit, it is
+possible to use Prolog based rules to provide project specific submit rules and
+replace the default submit rules. Using Prolog based rules, project owners can
+define a set of criteria which must be fulfilled for a change to become
+submittable. For a change that is not submittable, the set of needed criteria
+is displayed in the Gerrit UI.
+
+NOTE: Loading and executing Prolog submit rules may be disabled by setting
+`rules.enabled=false` in the Gerrit config file (see
+link:config-gerrit.html#_a_id_rules_a_section_rules[rules section])
+
+link:https://groups.google.com/d/topic/repo-discuss/wJxTGhlHZMM/discussion[This
+discussion thread] explains why Prolog was chosen for the purpose of writing
+project specific submit rules.
+link:http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.html[Gerrit
+2.2.2 ReleaseNotes] introduces Prolog support in Gerrit.
+
+Prolog Language
+---------------
+This document is not a complete Prolog tutorial.
+link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a
+good starting point for learning the Prolog language. This document will only explain
+some elements of Prolog that are necessary to understand the provided examples.
+
+Prolog in Gerrit
+----------------
+Gerrit uses its own link:https://code.google.com/p/prolog-cafe/[fork] of the
+original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe]
+project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs at
+runtime.
+
+Interactive Prolog Cafe Shell
+-----------------------------
+For interactive testing and playing with Prolog, Gerrit provides the
+link:pgm-prolog-shell.html[prolog-shell] program which opens an interactive
+Prolog interpreter shell.
+
+NOTE: The interactive shell is just a prolog shell, it does not load
+a gerrit server environment and thus is not intended for xref:TestingSubmitRules[testing submit rules].
+
+SWI-Prolog
+----------
+Instead of using the link:pgm-prolog-shell.html[prolog-shell] program one can
+also use the link:http://www.swi-prolog.org/[SWI-Prolog] environment. It
+provides a better shell interface and a graphical source-level debugger.
+
+The rules.pl file
+-----------------
+This section explains how to create and edit project specific submit rules. How
+to actually write the submit rules is explained in the next section.
+
+Project specific submit rules are stored in the `rules.pl` file in the
+`refs/meta/config` branch of that project. Therefore, we need to fetch and
+checkout the `refs/meta/config` branch in order to create or edit the `rules.pl`
+file:
+
+====
+ $ git fetch origin refs/meta/config:config
+ $ git checkout config
+ ... edit or create the rules.pl file
+ $ git add rules.pl
+ $ git commit -m "My submit rules"
+ $ git push origin HEAD:refs/meta/config
+====
+
+How to write submit rules
+-------------------------
+Whenever Gerrit needs to evaluate submit rules for a change `C` from project `P` it
+will first initialize the embedded Prolog interpreter by:
+
+* consulting a set of facts about the change `C`
+* consulting the `rules.pl` from the project `P`
+
+Conceptually we can imagine that Gerrit adds a set of facts about the change
+`C` on top of the `rules.pl` file and then consults it. The set of facts about
+the change `C` will look like:
+
+====
+ :- package gerrit. <1>
+
+ commit_author(user(1000000), 'John Doe', 'john.doe@example.com'). <2>
+ commit_committer(user(1000000), 'John Doe', 'john.doe@example.com'). <3>
+ commit_message('Add plugin support to Gerrit'). <4>
+ ...
+====
+
+<1> Gerrit will provide its facts in a package named `gerrit`. This means we
+have to use qualified names when writing our code and referencing these facts.
+For example: `gerrit:commit_author(ID, N, M)`
+<2> user ID, full name and email address of the commit author
+<3> user ID, full name and email address of the commit committer
+<4> commit message
+
+A complete set of facts which Gerrit provides about the change is listed in the
+link:prolog-change-facts.html[Prolog Facts for Gerrit Change].
+
+By default, Gerrit will search for a `submit_rule/1` predicate in the `rules.pl`
+file, evaluate the `submit_rule(X)` and then inspect the value of `X` in order
+to decide whether the change is submittable or not and also to find the set of
+needed criteria for the change to become submittable. This means that Gerrit has an
+expectation on the format and value of the result of the `submit_rule` predicate
+which is expected to be a `submit` term of the following format:
+
+====
+ submit(label(label-name, status) [, label(label-name, status)]*)
+====
+
+where `label-name` is usually `'Code-Review'` or `'Verified'` but could also
+be any other string (see examples below). The `status` is one of:
+
+* `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
+ used to tell that this label/category has been met.
+* `need(_)` is used to tell that this label/category is needed for change to
+ become submittable
+* `reject(user(ID))` or just `reject(_)`. This status is used to tell that label/category
+ is blocking change submission
+* `impossible(_)` is used when the logic knows that the change cannot be submitted as-is.
+ Administrative intervention is probably required. This is meant for cases
+ where the logic requires members of "FooEng" to score "Code-Review +2" on a
+ change, but nobody is in group "FooEng". It is to hint at permissions
+ misconfigurations.
+* `may(_)` allows expression of approval categories that are optional, i.e.
+ could either be set or unset without ever influencing whether the change
+ could be submitted.
+
+NOTE: For a change to be submittable all `label` terms contained in the returned
+`submit` term must have either `ok` or `may` status.
+
+IMPORTANT: Gerrit will let the Prolog engine continue searching for solutions of
+the `submit_rule(X)` query until it finds the first one where all labels in the
+return result have either status `ok` or `may` or there are no more solutions.
+If a solution where all labels have status `ok` is found then all previously
+found solutions are ignored. Otherwise, all labels names with status `need`
+from all solutions will be displayed in the UI indicating the set of conditions
+needed for the change to become submittable.
+
+Here some examples of possible return values from the `submit_rule` predicate:
+
+====
+ submit(label('Code-Review', ok(_))) <1>
+ submit(label('Code-Review', ok(_)), label('Verified', reject(_))) <2>
+ submit(label('Author-is-John-Doe', need(_)) <3>
+====
+
+<1> label `'Code-Review'` is met. As there are no other labels in the
+ return result, the change is submittable.
+<2> label `'Verified'` is rejected. Change is not submittable.
+<3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
+ Note that this tells nothing about how this criteria will be met. It is up
+ to the implementor of the `submit_rule` to return `label('Author-is-John-Doe',
+ ok(_))` when this criteria is met. Most likely, it will have to match
+ against `gerrit:commit_author` in order to check if this criteria is met.
+ This will become clear through the examples below.
+
+Of course, when implementing the `submit_rule` we will use the facts about the
+change that are already provided by Gerrit.
+
+Another aspect of the return result from the `submit_rule` predicate is that
+Gerrit uses it to decide which set of labels to display on the change review
+screen for voting. If the return result contains label `'ABC'` and if the label
+`'ABC'` is one of the (global) voting categories then voting for the label
+`'ABC'` will be displayed. Otherwise, it is not displayed. Note that we don't
+need a (global) voting category for each label contained in the result of
+`submit_rule` predicate. For example, the decision whether `'Author-is-John-Doe'`
+label is met will probably not be made by explicit voting but, instead, by
+inspecting the facts about the change.
+
+Submit Filter
+-------------
+Another mechanism of changing the default submit rules is to implement the
+`submit_filter/2` predicate. While Gerrit will search for the `submit_rule` only
+in the `rules.pl` file of the current project, the `submit_filter` will be
+searched for in the `rules.pl` of all parent projects of the current project,
+but not in the `rules.pl` of the current project. The search will start from the
+immediate parent of the current project, then in the parent project of that
+project and so on until, and including, the 'All-Projects' project.
+
+The purpose of the submit filter is, as its name says, to filter the results
+of the `submit_rule`. Therefore, the `submit_filter` predicate has two
+parameters:
+
+====
+ submit_filter(In, Out) :- ...
+====
+
+Gerrit will invoke `submit_filter` with the `In` parameter containing a `submit`
+structure produced by the `submit_rule` and will take the value of the `Out`
+parameter as the result.
+
+The `Out` value of a `submit_filter` will become the `In` value for the
+next `submit_filter` in the parent line. The value of the `Out` parameter
+of the top-most `submit_filter` is the final result of the submit rule that
+is used to decide whether a change is submittable or not.
+
+IMPORTANT: `submit_filter` is a mechanism for Gerrit administrators to implement
+and enforce submit rules that would apply to all projects while `submit_rule` is
+a mechanism for project owners to implement project specific submit rules.
+However, project owners who own several projects could also make use of
+`submit_filter` by using a common parent project for all their projects and
+implementing the `submit_filter` in this common parent project. This way they
+can avoid implementing the same `submit_rule` in all their projects.
+
+The following "drawing" illustrates the order of the invocation and the chaining
+of the results of the `submit_rule` and `submit_filter` predicates.
+
+====
+ All-Projects
+ ^ submit_filter(B, S) :- ... <4>
+ |
+ Parent-3
+ ^ <no submit filter here>
+ |
+ Parent-2
+ ^ submit_filter(A, B) :- ... <3>
+ |
+ Parent-1
+ ^ submit_filter(X, A) :- ... <2>
+ |
+ MyProject
+ submit_rule(X) :- ... <1>
+====
+
+<1> The `submit_rule` of `MyProject` is invoked first.
+<2> The result `X` is filtered through the `submit_filter` from the `Parent-1`
+project.
+<3> The result of `submit_filter` from `Parent-1` project is filtered by the
+`submit_filter` in the `Parent-2` project. Since `Parent-3` project doesn't have
+a `submit_filter` it is skipped.
+<4> The result of `submit_filter` from `Parent-2` project is filtered by the
+`submit_filter` in the `All-Projects` project. The value in `S` is the final
+value of the submit rule evaluation.
+
+NOTE: If `MyProject` doesn't define its own `submit_rule` Gerrit will invoke the
+default implementation of submit rule that is named `gerrit:default_submit` and
+its result will be filtered as described above.
+
+[[TestingSubmitRules]]
+Testing submit rules
+--------------------
+The prolog environment running the `submit_rule` is loaded with state describing the
+change that is being evaluated. The easiest way to load this state is to test your
+`submit_rule` against a real change on a running gerrit instance. The command
+link:cmd-test-submit-rule.html[test-submit-rule] loads a specific change and executes
+the `submit_rule`. It optionally reads the rule from from `stdin` to facilitate easy testing.
+
+====
+ cat rules.pl | ssh gerrit_srv gerrit test-submit-rule I45e080b105a50a625cc8e1fb5b357c0bfabe6d68 -s
+====
+
+Prolog vs Gerrit plugin for project specific submit rules
+---------------------------------------------------------
+Since version 2.5 Gerrit supports plugins and extension points. A plugin or an
+extension point could also be used as another means to provide custom submit
+rules. One could ask for a guideline when to use Prolog based submit rules and
+when to go for writing a new plugin. Writing a Prolog program is usually much
+faster than writing a Gerrit plugin. Prolog based submit rules can be pushed
+to a project by project owners while Gerrit plugins could only be installed by
+Gerrit administrators. In addition, Prolog based submit rules can be pushed
+for review by pushing to `refs/for/refs/meta/config` branch.
+
+On the other hand, Prolog based submit rules get a limited amount of facts about
+the change exposed to them. Gerrit plugins get full access to Gerrit internals
+and can potentially check more things than Prolog based rules.
+
+Examples
+--------
+The following examples should serve as a cookbook for developing own submit rules.
+Some of them are too trivial to be used in production and their only purpose is
+to provide step by step introduction and understanding.
+
+Some of the examples will implement the `submit_rule` and some will implement
+the `submit_filter` just to show both possibilities. Remember that
+`submit_rule` is only invoked from the current project and `submit_filter` is
+invoked from all parent projects. This is the most important fact in deciding
+whether to implement `submit_rule` or `submit_filter`.
+
+Example 1: Make every change submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Let's start with a most trivial example where we would make every change submittable
+regardless of the votes it has:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Any-Label-Name', ok(_)))).
+====
+
+In this case we make no use of facts about the change. We don't need it as we are simply
+making every change submittable. Note that, in this case, the Gerrit UI will not show
+the UI for voting for the standard `'Code-Review'` and `'Verified'` categories as labels
+with these names are not part of the return result. The `'Any-Label-Name'` could really
+be any string.
+
+Example 2: Every change submittable and voting in the standard categories possible
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This is continuation of the previous example where, in addition, to making
+every change submittable we want to enable voting in the standard
+`'Code-Review'` and `'Verified'` categories.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Code-Review', ok(_)), label('Verified', ok(_)))).
+====
+
+Since for every change all label statuses are `'ok'` every change will be submittable.
+Voting in the standard labels will be shown in the UI as the standard label names are
+included in the return result.
+
+Example 3: Nothing is submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example shows how to make all changes non-submittable regardless of the
+votes they have.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Any-Label-Name', reject(_)))).
+====
+
+Since for any change we return only one label with status `reject`, no change
+will be submittable. The UI will, however, not indicate what is needed for a
+change to become submittable as we return no labels with status `need`.
+
+Example 4: Nothing is submittable but UI shows several 'Need ...' criteria
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example no change is submittable but here we show how to present 'Need
+<label>' information to the user in the UI.
+
+.rules.pl
+[caption=""]
+====
+ % In the UI this will show: Need Any-Label-Name
+ submit_rule(submit(label('Any-Label-Name', need(_)))).
+
+ % We could define more "need" labels by adding more rules
+ submit_rule(submit(label('Another-Label-Name', need(_)))).
+
+ % or by providing more than one need label in the same rule
+ submit_rule(submit(label('X-Label-Name', need(_)), label('Y-Label-Name', need(_)))).
+====
+
+In the UI this will show:
+****
+* Need Any-Label-Name
+* Need Another-Label-Name
+* Need X-Label-Name
+* Need Y-Label-Name
+****
+
+From the example above we can see a few more things:
+
+* comment in Prolog starts with the `%` character
+* there could be multiple `submit_rule` predicates. Since Prolog, by default, tries to find
+ all solutions for a query, the result will be union of all solutions.
+ Therefore, we see all 4 `need` labels in the UI.
+
+Example 5: The 'Need ...' labels not shown when change is submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example shows that, when there is a solution for `submit_rule(X)` where all labels
+have status `ok` then Gerrit will not show any labels with the `need` status from
+any of the previous `submit_rule(X)` solutions.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(label('Some-Condition', need(_))).
+ submit_rule(label('Another-Condition', ok(_))).
+====
+
+The 'Need Some-Condition' will not be show in the UI because of the result of
+the second rule.
+
+The same is valid if the two rules are swapped:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(label('Another-Condition', ok(_))).
+ submit_rule(label('Some-Condition', need(_))).
+====
+
+The result of the first rule will stop search for any further solutions.
+
+Example 6: Make change submittable if commit author is "John Doe"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This is the first example where we will use the Prolog facts about a change that
+are automatically exposed by Gerrit. Our goal is to make any change submittable
+when the commit author is named `'John Doe'`. In the very first
+step let's make sure Gerrit UI shows 'Need Author-is-John-Doe' in
+the UI to clearly indicate to the user what is needed for a change to become
+submittable:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+====
+
+This will show:
+****
+* Need Author-is-John-Doe
+****
+
+in the UI but no change will be submittable yet. Let's add another rule:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+ submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+ :- gerrit:commit_author(_, 'John Doe', _).
+====
+
+In the second rule we return `ok` status for the `'Author-is-John-Doe'` label
+if there is a `commit_author` fact where the full name is `'John Doe'`. If
+author of a change is `'John Doe'` then the second rule will return a solution
+where all labels have `ok` status and the change will become submittable. If
+author of a change is not `'John Doe'` then only the first rule will produce a
+solution. The UI will show 'Need Author-is-John-Doe' but, as expected, the
+change will not be submittable.
+
+Instead of checking by full name we could also check by the email address:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+ submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+ :- gerrit:commit_author(_, _, 'john.doe@example.com').
+====
+
+or by user id (assuming it is 1000000):
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+ submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+ :- gerrit:commit_author(user(1000000), _, _).
+====
+
+or by a combination of these 3 attributes:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+ submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+ :- gerrit:commit_author(_, 'John Doe', 'john.doe@example.com').
+====
+
+Example 7: Make change submittable if commit message starts with "Trivial fix"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Besides showing how to make use of the commit message text the purpose of this
+example is also to show how to match only a part of a string symbol. Similarly
+like commit author the commit message is provided as a string symbol which is
+an atom in Prolog terms. When working with an atom we could only match against
+the whole value. To match only part of a string symbol we have, at least, two
+options:
+
+* convert the string symbol into a list of characters and then perform
+ the "classical" list matching
+* use the `regex_matches/2` or, even more convenient, the
+ `gerrit:commit_message_matches/1` predicate
+
+Let's implement both options:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
+ submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
+ :- gerrit:commit_message(M), name(M, L), starts_with(L, "Trivial Fix").
+
+ starts_with(L, []).
+ starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
+====
+
+NOTE: The `name/2` embedded predicate is used to convert a string symbol into a
+list of characters. A string `abc` is converted into a list of characters `[97,
+98, 99]`. A double quoted string in Prolog is just a shortcut for creating a
+list of characters. `"abc"` is a shortcut for `[97, 98, 99]`. This is why we use
+double quotes for the `"Trivial Fix"` in the example above.
+
+The `starts_with` predicate is self explaining.
+
+Using the `gerrit:commit_message_matches` predicate is probably more efficient:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
+ submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
+ :- gerrit:commit_message_matches('^Trivial Fix').
+====
+
+Reusing the default submit policy
+---------------------------------
+All examples until now concentrate on one particular aspect of change data.
+However, in real-life scenarios we would rather want to reuse Gerrit's default
+submit policy and extend/change it for our specific purpose. In other words, we
+would like to keep all the default policies (like the `Verified` category,
+vetoing change, etc...) and only extend/change an aspect of it. For example, we
+may want to disable the ability for change authors to approve their own changes
+but keep all other policies the same.
+
+To get results of Gerrits default submit policy we use the
+`gerrit:default_submit` predicate. This means that if we write a submit rule like:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(X) :- gerrit:default_submit(X).
+====
+
+then this is equivalent to not using `rules.pl` at all. We just delegate to
+default logic. However, once we invoke the `gerrit:default_submit(X)` we can
+perform further actions on the return result `X` and apply our specific
+logic. The following pattern illustrates this technique:
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(S) :- gerrit:default_submit(R), project_specific_policy(R, S).
+
+ project_specific_policy(R, S) :- ...
+====
+
+The following examples build on top of the default submit policy.
+
+Example 8: Make change submittable only if `Code-Review+2` is given by a non author
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we introduce a new label `Non-Author-Code-Review` and make it
+satisfied if there is at least one `Code-Review+2` from a non author. All other
+default policies like the `Verified` category and vetoing changes still apply.
+
+First, we invoke `gerrit:default_submit` to compute the result for the default
+submit policy and then add the `Non-Author-Code-Review` label to it. The
+`Non-Author-Code-Review` label is added with status `ok` if such an approval
+exists or with status `need` if it doesn't exist.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(S) :-
+ gerrit:default_submit(X),
+ X =.. [submit | Ls],
+ add_non_author_approval(Ls, R),
+ S =.. [submit | R].
+
+ add_non_author_approval(S1, S2) :-
+ gerrit:commit_author(A), gerrit:commit_label(label('Code-Review', 2), R),
+ R \= A, !,
+ S2 = [label('Non-Author-Code-Review', ok(R)) | S1].
+ add_non_author_approval(S1, [label('Non-Author-Code-Review', need(_)) | S1]).
+====
+
+This example uses the `univ` operator `=..` to "unpack" the result of the
+default_submit, which is a structure of the form `submit(label('Code-Review',
+ok(_)), label('Verified', need(_)) ...)` into a list like `[submit,
+label('Code-Review', ok(_)), label('Verified', need(_)), ...]`. Then we
+process the tail of the list (the list of labels) as a Prolog list, which is
+much easier than processing a structure. In the end we use the same `univ`
+operator to convert the resulting list of labels back into a `submit` structure
+which is expected as a return result. The `univ` operator works both ways.
+
+In `add_non_author_approval` we use the `cut` operator `!` to prevent Prolog
+from searching for more solutions once the `cut` point is reached. This is
+important because in the second `add_non_author_approval` rule we just add the
+`label('Non-Author-Code-Review', need(_))` without first checking that there
+is no non author `Code-Review+2`. The second rule will only be reached
+if the `cut` in the first rule is not reached and it only happens if a
+predicate before the `cut` fails.
+
+Example 9: Remove the `Verified` category
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A project has no build and test. It consists of only text files and needs only
+code review. We want to remove the `Verified` category from this project so
+that `Code-Review+2` is the only criteria for a change to become submittable.
+We also want the UI to not show the `Verified` category in the table with
+votes and on the voting screen.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(S) :-
+ gerrit:default_submit(X),
+ X =.. [submit | Ls],
+ remove_verified_category(Ls, R),
+ S =.. [submit | R].
+
+ remove_verified_category([], []).
+ remove_verified_category([label('Verified', _) | T], R) :- remove_verified_category(T, R), !.
+ remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+====
+
+Example 10: Combine examples 8 and 9
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we want to both remove the verified and have the four eyes
+principle. This means we want a combination of examples 7 and 8.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(S) :-
+ gerrit:default_submit(X),
+ X =.. [submit | Ls],
+ remove_verified_category(Ls, R1),
+ add_non_author_approval(R1, R),
+ S =.. [submit | R].
+====
+
+The `remove_verified_category` and `add_non_author_approval` predicates are the
+same as defined in the previous two examples.
+
+Example 11: Remove the `Verified` category from all projects
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Example 9, implements `submit_rule` that removes the `Verified` category from
+one project. In this example we do the same but we want to remove the `Verified`
+category from all projects. This means we have to implement `submit_filter` and
+we have to do that in the `rules.pl` of the `All-Projects` project.
+
+.rules.pl
+[caption=""]
+====
+ submit_filter(In, Out) :-
+ In =.. [submit | Ls],
+ remove_verified_category(Ls, R),
+ Out =.. [submit | R].
+
+ remove_verified_category([], []).
+ remove_verified_category([label('Verified', _) | T], R) :-
+ remove_verified_category(T, R), !.
+ remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+====
+
+Example 12: 1+1=2 Code-Review
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we introduce accumulative voting to determine if a change is
+submittable or not. We modify the standard Code-Review to be accumulative, and make the
+change submittable if the total score is 2 or higher.
+
+The code in this example is very similar to Example 8, with the addition of findall/3
+and gerrit:remove_label.
+The findall/3 embedded predicate is used to form a list of all objects that satisfy a
+specified Goal. In this example it is used to get a list of all the 'Code-Review' scores.
+gerrit:remove_label is a built-in helper that is implemented similarly to the
+'remove_verified_category' as seen in the previous example.
+
+.rules.pl
+[caption=""]
+====
+ sum_list([], 0).
+ sum_list([H | Rest], Sum) :- sum_list(Rest,Tmp), Sum is H + Tmp.
+
+ add_category_min_score(In, Category, Min, P) :-
+ findall(X, gerrit:commit_label(label(Category,X),R),Z),
+ sum_list(Z, Sum),
+ Sum >= Min, !,
+ P = [label(Category,ok(R)) | In].
+
+ add_category_min_score(In, Category,Min,P) :-
+ P = [label(Category,need(Min)) | In].
+
+ submit_rule(S) :-
+ gerrit:default_submit(X),
+ X =.. [submit | Ls],
+ gerrit:remove_label(Ls,label('Code-Review',_),NoCR),
+ add_category_min_score(NoCR,'Code-Review', 2, Labels),
+ S =.. [submit | Labels].
+====
+
+Example 13: Master and apprentice
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The master and apprentice example allow you to specify a user (the `master`)
+that must approve all changes done by another user (the `apprentice`).
+
+The code first checks if the commit author is in the apprentice database.
+If the commit is done by an apprentice, it will check if there is a +2
+review by the associated `master`.
+
+.rules.pl
+[caption=""]
+====
+ % master_apprentice(Master, Apprentice).
+ % Extend this with appropriate user-id's for your master/apprentice setup.
+ master_apprentice(user(1000064), user(1000000)).
+
+ submit_rule(S) :-
+ gerrit:default_submit(In),
+ In =.. [submit | Ls],
+ add_apprentice_master(Ls, R),
+ S =.. [submit | R].
+
+ check_master_approval(S1, S2, Master) :-
+ gerrit:commit_label(label('Code-Review', 2), R),
+ R = Master, !,
+ S2 = [label('Master-Approval', ok(R)) | S1].
+ check_master_approval(S1, [label('Master-Approval', need(_)) | S1], _).
+
+ add_apprentice_master(S1, S2) :-
+ gerrit:commit_author(Id),
+ master_apprentice(Master, Id),
+ !,
+ check_master_approval(S1, S2, Master).
+
+ add_apprentice_master(S, S).
+====
+
+Example 14: Only allow Author to submit change
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example adds a new needed category `Patchset-Author` for any user that is
+not the author of the patch. This effectively blocks all users except the author
+from submitting the change. This could result in an impossible situation if the
+author does not have permissions for submitting the change.
+
+.rules.pl
+[caption=""]
+====
+ submit_rule(S) :-
+ gerrit:default_submit(In),
+ In =.. [submit | Ls],
+ only_allow_author_to_submit(Ls, R),
+ S =.. [submit | R].
+
+ only_allow_author_to_submit(S, S) :-
+ gerrit:commit_author(Id),
+ gerrit:current_user(Id),
+ !.
+
+ only_allow_author_to_submit(S1, [label('Patchset-Author', need(_)) | S1]).
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/refs-notes-review.txt b/Documentation/refs-notes-review.txt
new file mode 100644
index 0000000000..632f5673a4
--- /dev/null
+++ b/Documentation/refs-notes-review.txt
@@ -0,0 +1,111 @@
+The refs/notes/review namespace
+===============================
+
+Summary
+-------
+
+`refs/notes/review` is a special reference that Gerrit creates on repositories
+to store information about code reviews.
+
+When a repository is cloned from Gerrit, the `refs/notes/review` reference is
+not included by default. It has to be manually fetched:
+
+====
+ $ git fetch origin refs/notes/review:refs/notes/review
+====
+
+It is also possible to
+link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[configure git]
+to always fetch `refs/notes/review`:
+
+====
+ $ git config --add remote.origin.fetch refs/notes/review:refs/notes/review
+ $ git fetch
+====
+
+When `refs/notes/review` is fetched on a repository, the Gerrit review
+information can be included in the git log output:
+
+====
+ $ git log --show-notes=review
+====
+
+Content of refs/notes/review
+----------------------------
+
+For each commit, Gerrit stores the following review information in
+`refs/notes/review`:
+
+[[submitted_by]]
+Submitted-by
+~~~~~~~~~~~~
+
+The name and email address of the Gerrit user that submitted the change in
+link:http://www.ietf.org/rfc/rfc2822.txt[RFC 2822] format.
+
+====
+ Submitted-by: Random J Developer <random@developer.example.org>
+====
+
+[[submitted_at]]
+Submitted-at
+~~~~~~~~~~~~
+
+The time the commit was submitted in RFC 2822 time stamp format.
+
+====
+ Submitted-at: Mon, 25 Jun 2012 16:15:57 +0200
+====
+
+[[reviewed_on]]
+Reviewed-on
+~~~~~~~~~~~
+
+The URL to the change on the Gerrit server.
+
+====
+ Reviewed-on: http://path.to.gerrit/12345
+====
+
+[[review_scores]]
+Review Labels and Scores
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Review label and score, and the name and email address of the Gerrit user that
+gave it in RFC 2822 format:
+
+====
+ Code-Review+2: A. N. Other <another@developer.example.org>
+ Verified+1: A. N. Other <another@developer.example.org>
+====
+
+Commonly used review labels are "Code-Review" and "Verified", but any label
+configured in Gerrit can be included.
+
+All review labels and scores present on the change at the time of submit are
+included.
+
+[[project]]
+Project
+~~~~~~~
+
+The name of the project in which the commit was made.
+
+====
+ Project: kernel/common
+====
+
+[[branch]]
+Branch
+~~~~~~
+
+The name of the branch on which the commit was made.
+
+====
+ Branch: refs/heads/master
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
new file mode 100644
index 0000000000..2f9d03ff3d
--- /dev/null
+++ b/Documentation/rest-api.txt
@@ -0,0 +1,393 @@
+Gerrit Code Review - REST API
+=============================
+
+Gerrit Code Review comes with a REST like API available over HTTP.
+The API is suitable for automated tools to build upon, as well as
+supporting some ad-hoc scripting use cases.
+
+Protocol Details
+----------------
+
+[[authentication]]
+Authentication
+~~~~~~~~~~~~~~
+By default all REST endpoints assume anonymous access and filter
+results to correspond to what anonymous users can read (which may
+be nothing at all).
+
+Users (and programs) may authenticate using HTTP authentication by
+supplying the HTTP password from the user's account settings page.
+Gerrit by default uses HTTP digest authentication. To authenticate,
+prefix the endpoint URL with `/a/`. For example to authenticate to
+`/projects/` request URL `/a/projects/`.
+
+[[output]]
+Output Format
+~~~~~~~~~~~~~
+Most APIs return text format by default. JSON can be requested
+by setting the `Accept` HTTP request header to include
+`application/json`, for example:
+
+----
+ GET /projects/ HTTP/1.0
+ Accept: application/json
+----
+
+JSON responses are encoded using UTF-8 and use content type
+`application/json`. The JSON response body starts with a magic prefix
+line that must be stripped before feeding the rest of the response
+body to a JSON parser:
+
+----
+ )]}'
+ [ ... valid JSON ... ]
+----
+
+The default JSON format is `JSON_COMPACT`, which skips unnecessary
+whitespace. This is not the easiest format for a human to read. Many
+examples in this documentation use `format=JSON` as a query parameter
+to obtain pretty formatting in the response. Producing (and parsing)
+the compact format is more efficient, so most tools should prefer the
+default compact format.
+
+Responses will be gzip compressed by the server if the HTTP
+`Accept-Encoding` request header is set to `gzip`. This may
+save on network transfer time for larger responses.
+
+Endpoints
+---------
+
+[[accounts_self_capabilities]]
+/accounts/self/capabilities (Account Capabilities)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns the global capabilities (such as `createProject` or
+`createGroup`) that are enabled for the calling user. This can be used
+by UI tools to discover if administrative features are available
+to the caller, so they can hide (or show) relevant UI actions.
+
+----
+ GET /accounts/self/capabilities?format=JSON HTTP/1.0
+
+ )]}'
+ {
+ "queryLimit": {
+ "min": 0,
+ "max": 500
+ }
+ }
+----
+
+Administrator that has authenticated with digest authentication:
+----
+ GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
+ Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+ )]}'
+ {
+ "administrateServer": true,
+ "queryLimit": {
+ "min": 0,
+ "max": 500
+ },
+ "createAccount": true,
+ "createGroup": true,
+ "createProject": true,
+ "killTask": true,
+ "viewCaches": true,
+ "flushCaches": true,
+ "viewConnections": true,
+ "viewQueue": true,
+ "startReplication": true
+ }
+----
+
+To filter the set of global capabilities the `q` parameter can be used.
+Filtering may decrease the response time by avoiding looking at every
+possible alternative for the caller.
+
+----
+ GET /a/accounts/self/capabilities?format=JSON&q=createAccount&q=createGroup HTTP/1.0
+ Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+ )]}'
+ {
+ "createAccount": true,
+ "createGroup": true
+ }
+----
+
+Most results are boolean, and a field is only present when its value
+is `true`. link:json.html#queryLimit[`queryLimit`] is a range and is
+presented as a nested JSON object with `min` and `max` members.
+
+[[projects]]
+/projects/ (List Projects)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Lists the projects accessible by the caller. This is the same as
+using the link:cmd-ls-projects.html[ls-projects] command over SSH,
+and accepts the same options as query parameters.
+
+----
+ GET /projects/?format=JSON&d HTTP/1.0
+
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "external/bison": {
+ "description": "GNU parser generator"
+ },
+ "external/gcc": {},
+ "external/openssl": {
+ "description": "encryption\ncrypto routines"
+ },
+ "test": {
+ "description": "\u003chtml\u003e is escaped"
+ }
+ }
+----
+
+[[suggest-projects]]
+The `/projects/` URL also accepts a prefix string as part of the URL.
+This limits the results to those projects that start with the specified
+prefix.
+List all projects that start with `platform/`:
+----
+GET /projects/platform/?format=JSON HTTP/1.0
+HTTP/1.1 200 OK
+Content-Disposition: attachment
+Content-Type: application/json;charset=UTF-8
+)]}'
+{
+"platform/drivers": {},
+"platform/tools": {}
+}
+----
+E.g. this feature can be used by suggestion client UI's to limit results.
+
+[[changes]]
+/changes/ (Query Changes)
+~~~~~~~~~~~~~~~~~~~~~~~~~
+Queries changes visible to the caller. The query string must be
+provided by the `q` parameter. The `n` parameter can be used to limit
+the returned results.
+
+Query for open changes of watched projects:
+----
+ GET /changes/?format=JSON&q=status:open+is:watched&n=2 HTTP/1.0
+
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "project": "demo",
+ "branch": "master",
+ "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+ "subject": "One change",
+ "status": "NEW",
+ "created": "2012-07-17 07:18:30.854000000",
+ "updated": "2012-07-17 07:19:27.766000000",
+ "reviewed": true,
+ "_sortkey": "001e7057000006dc",
+ "_number": 1756,
+ "owner": {
+ "name": "John Doe"
+ },
+ },
+ {
+ "project": "demo",
+ "branch": "master",
+ "id": "I09c8041b5867d5b33170316e2abc34b79bbb8501",
+ "subject": "Another change",
+ "status": "NEW",
+ "created": "2012-07-17 07:18:30.884000000",
+ "updated": "2012-07-17 07:18:30.885000000",
+ "_sortkey": "001e7056000006dd",
+ "_number": 1757,
+ "owner": {
+ "name": "John Doe"
+ },
+ "_more_changes": true
+ }
+----
+
+The change output is sorted by the last update time, most recently
+updated to oldest update.
+
+If the `n` query parameter is supplied and additional changes exist
+that match the query beyond the end, the last change object has a
+`_more_changes: true` JSON field set. Callers can resume a query with
+the `n` query parameter, supplying the last change's `_sortkey` field
+as the value. When going in the reverse direction with the `p` query
+parameter a `_more_changes: true` is put in the first change object if
+there are results *before* the first change returned.
+
+Clients are allowed to specify more than one query by setting the `q`
+parameter multiple times. In this case the result is an array of
+arrays, one per query in the same order the queries were given in.
+
+Query that retrieves changes for a user's dashboard:
+----
+ GET /changes/?format=JSON&q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
+
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ [
+ {
+ "project": "demo",
+ "branch": "master",
+ "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+ "subject": "One change",
+ "status": "NEW",
+ "created": "2012-07-17 07:18:30.854000000",
+ "updated": "2012-07-17 07:19:27.766000000",
+ "reviewed": true,
+ "_sortkey": "001e7057000006dc",
+ "_number": 1756,
+ "owner": {
+ "name": "John Doe"
+ },
+ "labels": {
+ "Verified": {},
+ "Code-Review": {}
+ }
+ }
+ ],
+ [],
+ []
+ ]
+----
+
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Optional fields are:
+
+* `LABELS`: a summary of each label required for submit, and
+ approvers that have granted (or rejected) with that label.
+
+* `CURRENT_REVISION`: describe the current revision (patch set)
+ of the change, including the commit SHA-1 and URLs to fetch from.
+
+* `ALL_REVISIONS`: describe all revisions, not just current.
+
+* `CURRENT_COMMIT`: parse and output all header fields from the
+ commit object, including message. Only valid when the current
+ revision or all revisions are selected.
+
+* `ALL_COMMITS`: parse and output all header fields from the
+ output revisions. If only `CURRENT_REVISION` was requested
+ then only the current revision's commit data will be output.
+
+* `CURRENT_FILES`: list files modified by the commit, including
+ basic line counts inserted/deleted per file. Only valid when
+ the current revision or all revisions are selected.
+
+* `ALL_FILES`: list files modified by the commit, including
+ basic line counts inserted/deleted per file. If only the
+ `CURRENT_REVISION` was requested the only that commit's
+ modified files will be output.
+
+----
+ GET /changes/?q=97&format=JSON&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
+
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ [
+ {
+ "project": "gerrit",
+ "branch": "master",
+ "id": "I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
+ "subject": "Use an EventBus to manage star icons",
+ "status": "NEW",
+ "created": "2012-04-25 00:52:25.580000000",
+ "updated": "2012-04-25 00:52:25.586000000",
+ "_sortkey": "001c9bf400000061",
+ "_number": 97,
+ "owner": {
+ "name": "Shawn Pearce"
+ },
+ "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
+ "revisions": {
+ "184ebe53805e102605d11f6b143486d15c23a09c": {
+ "_number": 1,
+ "fetch": {
+ "git": {
+ "url": "git://localhost/gerrit",
+ "ref": "refs/changes/97/97/1"
+ },
+ "http": {
+ "url": "http://127.0.0.1:8080/gerrit",
+ "ref": "refs/changes/97/97/1"
+ }
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+ "subject": "Migrate contributor agreements to All-Projects."
+ }
+ ],
+ "author": {
+ "name": "Shawn O. Pearce",
+ "email": "sop@google.com",
+ "date": "2012-04-24 18:08:08.000000000",
+ "tz": -420
+ },
+ "committer": {
+ "name": "Shawn O. Pearce",
+ "email": "sop@google.com",
+ "date": "2012-04-24 18:08:08.000000000",
+ "tz": -420
+ },
+ "subject": "Use an EventBus to manage star icons",
+ "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+ },
+ "files": {
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
+ "lines_deleted": 8
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
+ "lines_inserted": 1
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
+ "lines_inserted": 11,
+ "lines_deleted": 19
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
+ "lines_inserted": 23,
+ "lines_deleted": 20
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
+ "status": "D",
+ "lines_deleted": 139
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
+ "status": "A",
+ "lines_inserted": 204
+ },
+ "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
+ "lines_deleted": 9
+ }
+ }
+ }
+ }
+ }
+ ]
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 124ec315d3..a3015e163b 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -4,7 +4,7 @@ Gerrit Code Review - Change-Ids
Description
-----------
-Gerrit Code Review sometimes relies upon Change-Id lines in the
+Gerrit Code Review sometimes relies upon a Change-Id line at the
bottom of a commit message to uniquely identify a change across all
drafts of it. By including a unique Change-Id in the commit message,
Gerrit can automatically associate a new version of a change back
@@ -37,7 +37,7 @@ is the unique identity assigned to this change. It does not match
the commit name, `29a6...`, as the change may have been amended or
rebased to address reviewer comments since its initial inception.
-To avoid confusion with commit names, Change-Ids typically are with
+To avoid confusion with commit names, Change-Ids are typically prefixed with
an uppercase `I`.
Creation
@@ -46,11 +46,13 @@ Creation
Gerrit Code Review provides a standard 'commit-msg' hook which
can be installed in the local Git repository to automatically
create and insert a unique Change-Id line during `git commit`.
-To install the hook, copy it from Gerrit's daemon:
+To install the hook, copy it from Gerrit's daemon by executing
+one of the following commands while being in the root directory
+of the local Git repository:
$ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
- $ curl http://review.example.com/tools/hooks/commit-msg
+ $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
For more details, see link:cmd-hook-commit-msg.html[commit-msg].
@@ -125,7 +127,7 @@ When faced with multiple lines, try to preserve a line which was
already uploaded to Gerrit Code Review, and thus has a corresponding
change that reviewers have already examined and left comments on.
If you aren't sure which lines Gerrit knows about, try copying and
-pasting the lines into the search box in the top-right.
+pasting the lines into the search box at the top-right of the web interface.
If Gerrit already knows about more than one Change-Id, pick one
to keep in the squashed commit message, and manually abandon the
diff --git a/Documentation/user-custom-dashboards.txt b/Documentation/user-custom-dashboards.txt
new file mode 100644
index 0000000000..a015e4c3ae
--- /dev/null
+++ b/Documentation/user-custom-dashboards.txt
@@ -0,0 +1,48 @@
+Gerrit Code Review - Custom Dashboards
+======================================
+
+Description
+-----------
+
+A custom dashboard is shown in a layout similar to the per-user
+dashboard, but the sections are entirely configured from the URL.
+Because of this custom dashboards are stateless on the server side.
+Users or projects can simply trade URLs using an external system like
+a project wiki, or site administrators can put the links into the
+site's `GerritHeader.html` or `GerritFooter.html`.
+
+Dashboards are available via URLs like:
+----
+ /#/dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
+----
+This opens a view showing the title "Custom View" with two sections,
+"To Review" and "Pending in myproject":
+----
+ Custom View
+
+ To Review
+
+ Results of `reviewer:john.doe@example.com`
+
+ Pending In myproject
+
+ Results of `project:myproject is:open`
+----
+
+The dashboard URLs are easy to configure. All keys and values in the
+URL are encoded as query parameters. Set the page and window title
+using an optional `title=Text` parameter.
+
+Each section's title is defined by the parameter name, the section
+display order is defined by the order the parameters appear in the
+URL, and the query results are defined by the parameter value. To
+limit the number of rows in a query use `limit:N`, otherwise the
+entire result set will be shown (up to the user's query limit).
+
+Parameters may be separated from each other using any of the following
+characters, as some users may find one more readable than another:
+`&` or `;` or `,`
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
new file mode 100644
index 0000000000..ae3c2d09c5
--- /dev/null
+++ b/Documentation/user-notify.txt
@@ -0,0 +1,127 @@
+Gerrit Code Review - Email Notifications
+========================================
+
+Description
+-----------
+
+Gerrit can automatically notify users by email when new changes are
+uploaded for review, after comments have been posted on a change,
+or after the change has been submitted to a branch.
+
+User Level Settings
+-------------------
+
+Individual users can configure email subscriptions by editing
+watched projects through Settings > Watched Projects with the web UI.
+
+Specific projects may be watched, or the special project
+`All-Projects` can be watched to watch all projects that
+are visible to the user.
+
+Change search expressions can be used to filter change notifications
+to specific subsets, for example `branch:master` to only see changes
+proposed for the master branch.
+
+Project Level Settings
+----------------------
+
+Project owners and site administrators can configure project level
+notifications, enabling Gerrit Code Review to automatically send
+emails to team mailing lists, or groups of users. Project settings
+are stored inside of the `refs/meta/config` branch of each Git
+repository, and are placed inside of the `project.config` file.
+
+To edit the project level notify settings, ensure the project owner
+has Push permission already granted for the `refs/meta/config`
+branch. Consult link:access-control.html[access controls] for
+details on how access permissions work.
+
+Initialize a temporary Git repository to edit the configuration:
+====
+ mkdir cfg_dir
+ cd cfg_dir
+ git init
+====
+
+Download the existing configuration from Gerrit:
+====
+ git fetch ssh://localhost:29418/project refs/meta/config
+ git checkout FETCH_HEAD
+====
+
+Enable notifications to an email address by adding to
+`project.config`, this can be done using the `git config` command:
+====
+ git config -f project.config --add notify.team.email team-address@example.com
+ git config -f project.config --add notify.team.email paranoid-manager@example.com
+====
+
+Examining the project.config file with any text editor should show
+a new notify section describing the email addresses to deliver to:
+----
+ [notify "team"]
+ email = team-address@example.com
+ email = paranoid-manager@example.com
+----
+
+Each notify section within a single project.config file must have a
+unique name. The section name itself does not matter and may later
+appear in the web UI. Naming a section after the email address or
+group it delivers to is typical. Multiple sections can be specified
+if different filters are needed.
+
+Commit the configuration change, and push it back:
+====
+ git commit -a -m "Notify team-address@example.com of changes"
+ git push ssh://localhost:29418/project HEAD:refs/meta/config
+====
+
+[[notify.name.email]]notify.<name>.email::
++
+List of email addresses to send matching notifications to. Each
+email address should be placed on its own line.
++
+Internal groups within Gerrit Code Review can also be named using
+`group NAME` syntax. If this format is used the group's UUID must
+also appear in the corresponding `groups` file. Gerrit will expand
+the group membership and BCC all current users.
+
+[[notify.name.type]]notify.<name>.type::
++
+Types of notifications to send. If not specified, all notifications
+are sent.
++
+* `new_changes`: Only newly created changes.
+* `all_comments`: Only comments on existing changes.
+* `submitted_changes`: Only changes that have been submitted.
+* `all`: All notifications.
+
++
+Like email, this variable may be a list of options.
+
+[[notify.name.filter]]notify.<name>.filter::
++
+link:user-search.html[Change search expression] to match changes that
+should be sent to the emails named in this section. Within a Git-style
+configuration file double quotes around complex operator values may
+need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`.
+
+When sending email to a bare email address in a notify block, Gerrit
+Code Review ignores read access controls and assumes the administrator
+has set the filtering options correctly. Project owners can implement
+security filtering by adding the `visibleto:groupname` predicate to
+the filter expression, for example:
+
+====
+ [notify "Developers"]
+ email = team-address@example.com
+ filter = visibleto:Developers
+====
+
+When sending email to an internal group, the internal group's read
+access is automatically checked by Gerrit and therefore does not
+need to use the `visibleto:` operator in the filter.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3aafe8c164..8a231fe72e 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -16,9 +16,10 @@ matches the search, the change will be presented instead of a list.
|All > Open | status:open '(or is:open)'
|All > Merged | status:merged
|All > Abandoned | status:abandoned
-|My > Dafts | has:draft
+|My > Drafts | is:draft
|My > Watched Changes | status:open is:watched
|My > Starred Changes | is:starred
+|My > Draft Comments | has:draft
|Open changes in Foo | status:open project:Foo
|=================================================
@@ -75,7 +76,8 @@ Change-Id that was scraped out of the commit message.
[[owner]]
owner:'USER'::
+
-Changes originally submitted by 'USER'.
+Changes originally submitted by 'USER'. The special case of
+`owner:self` will find changes owned by the caller.
[[ownerin]]
ownerin:'GROUP'::
@@ -85,7 +87,9 @@ Changes originally submitted by a user in 'GROUP'.
[[reviewer]]
reviewer:'USER'::
+
-Changes that have been, or need to be, reviewed by 'USER'.
+Changes that have been, or need to be, reviewed by 'USER'. The
+special case of `reviewer:self` will find changes where the caller
+has been added as a reviewer.
[[reviewerin]]
reviewerin:'GROUP'::
@@ -145,7 +149,7 @@ library] is used for evaluation of such patterns.
[[tr,bug]]
tr:'ID', bug:'ID'::
+
-Search for changes whose commit message contains 'ID' and matched
+Search for changes whose commit message contains 'ID' and matches
one or more of the
link:config-gerrit.html#trackingid[trackingid sections]
in the server's configuration file. This is typically used to
@@ -162,7 +166,7 @@ of the argument.
[[message]]
message:'MESSAGE'::
+
-Changes that matches 'MESSAGE' arbitrary string in body commit messages.
+Changes that match 'MESSAGE' arbitrary string in the commit message body.
[[file]]
file:^'REGEX'::
@@ -213,9 +217,23 @@ is:reviewed::
True if there is at least one non-zero score on the change, in any
approval category, by any user.
+is:owner::
++
+True on any change where the current user is the change owner.
+Same as `owner:self`.
+
+is:reviewer::
++
+True on any change where the current user is a reviewer.
+Same as `reviewer:self`.
+
is:open::
+
-True if the change is other open or submitted, merge pending.
+True if the change is either open or submitted, merge pending.
+
+is:draft::
++
+True if the change is a draft.
is:closed::
+
@@ -228,7 +246,7 @@ Same as <<status,status:'STATE'>>.
[[status]]
status:open::
+
-True if the change state is other 'review in progress' or 'submitted,
+True if the change state is either 'review in progress' or 'submitted,
merge pending'.
status:reviewed::
@@ -250,7 +268,7 @@ Change has been merged into the branch.
status:abandoned::
+
-Change has been abandoned by the change owner, or administrator.
+Change has been abandoned.
Boolean Operators
@@ -286,7 +304,7 @@ that are returned, as more changes are considered.
[[labels]]
Labels
------
-Label operators can be used to match approval score given during
+Label operators can be used to match approval scores given during
a code review. The specific set of supported labels depends on
the server configuration, however `CodeReview` and `Verified`
are the default labels provided out of the box.
@@ -305,7 +323,7 @@ A label name is any of the following:
of change list pages. Example: `label:R` or `label:V`.
A label name must be followed by a score, or an operator and a score.
-The easiest way to explain these are by example.
+The easiest way to explain this is by example.
`label:CodeReview=2`::
`label:CodeReview=+2`::
@@ -373,16 +391,20 @@ the change. This flag is always added to any query.
starredby:'USER'::
+
Matches changes that have been starred by 'USER'.
+The special case `starredby:self` applies to the caller.
watchedby:'USER'::
+
Matches changes that 'USER' has configured watch filters for.
+The special case `watchedby:self` applies to the caller.
draftby:'USER'::
+
-Matches changes that 'USER' has left unpublished drafts on.
+Matches changes that 'USER' has left unpublished draft comments on.
Since the drafts are unpublished, it is not possible to see the
-draft text, or even how many drafts there are.
+draft text, or even how many drafts there are. The special case
+of `draftby:self` will find changes where the caller has created
+a draft comment.
limit:'CNT'::
+
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 3d14437add..cfaf3e981d 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -18,7 +18,7 @@ to identify if it registers git submodules (if the commit registers
any gitlinks and .gitmodules file with required info) and if so,
a new submodule subscription is registered.
-When a new commit of a registered submodule is merged, gerrit
+When a new commit of a registered submodule is merged, Gerrit
automatically updates the subscribers to the submodule with a new
commit having the updated gitlinks.
@@ -31,7 +31,7 @@ is to provide a brief overview, further details can be found
in the official git submodule command documentation.
Imagine a repository called 'super' and another one called 'a'.
-Also consider 'a' available in a running gerrit instance on "server".
+Also consider 'a' available in a running Gerrit instance on "server".
With this feature, one could attach 'a' inside of 'super' repository
at path 'a' by executing the following command when being inside
'super':
@@ -86,12 +86,12 @@ has the same name as the destination branch of the commit having
gitlinks/.gitmodules file.
The branch field of a submodule section is a custom git submodule
-feature for gerrit use. One should always be sure to fill it in
+feature for Gerrit use. One should always be sure to fill it in
editing .gitmodules file after adding submodules to a super project,
-if it is the intention to make use of the gerrit feature introduced here.
+if it is the intention to make use of the Gerrit feature introduced here.
Any git submodules which are added and not have the branch field
-available in the .gitmodules file will not be subscribed by gerrit
+available in the .gitmodules file will not be subscribed by Gerrit
to automatically update the superproject.
Detecting and Subscribing Submodules
@@ -114,7 +114,7 @@ is updated.
Imagine a superproject called 'super' having a branch called 'dev'
having subscribed to a submodule 'a' on a branch 'dev-of-a'. When a commit
-is merged in branch 'dev-of-a' of 'a' project, gerrit automatically
+is merged in branch 'dev-of-a' of 'a' project, Gerrit automatically
creates a new commit on branch 'dev' of 'super' updating the gitlink
to point to the just merged commit.
@@ -123,11 +123,11 @@ Canonical Web Url
Gerrit will automatically update only the superprojects that added
the submodules of urls of the running server (the one described in
-the canonical web url value in gerrit configuration file).
+the canonical web url value in Gerrit configuration file).
The Gerrit instance administrator group should always certify to
provide the canonical web url value in its configuration file. Users
-should certify to use the url value of the running gerrit instance to
+should certify to use the url value of the running Gerrit instance to
add/subscribe submodules.
Removing Subscriptions
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 8e05e726c5..6bdff7a6e1 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -46,7 +46,7 @@ and paste it into Gerrit's web interface:
[TIP]
Users who frequently upload changes will also want to consider
-starting a `ssh-agent`, and adding their private key to the list
+starting an `ssh-agent`, and adding their private key to the list
managed by the agent, to reduce the frequency of entering the
key's passphrase. Consult `man ssh-agent`, or your SSH client's
documentation, for more details on configuration of the agent
@@ -57,7 +57,7 @@ Testing Connections
~~~~~~~~~~~~~~~~~~~
To verify your SSH key is working correctly, try using an SSH client
-to connect to Gerrit's SSHD port. By default Gerrit is running on
+to connect to Gerrit's SSHD port. By default Gerrit runs on
port 29418, using the same hostname as the web server:
====
@@ -104,7 +104,7 @@ git push
Create Changes
~~~~~~~~~~~~~~
-To create new changes for review, simply push into the project's
+To create new changes for review, simply push to the project's
magical `refs/for/'branch'` ref using any Git client tool:
====
@@ -316,7 +316,7 @@ repo upload
repo is a multiple repository management tool, most commonly
used by the Android Open Source Project. For more details, see
-link:http://source.android.com/download/using-repo[using repo].
+link:http://source.android.com/source/using-repo.html[using repo].
[[repo_create]]
Create Changes
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 3f1f76f9bd..ddfe323043 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -33,7 +33,7 @@ uses the default_submit predicate in common_rules.pl.
+
Projects now inherit the prolog rules defined in their parent
project. Submit results from the child project are filtered by the
-parent project using the filter predicate defined the parent's
+parent project using the filter predicate defined in the parent's
rules.pl. The results of the filtering are then passed up to the
parent's parent and filtered, repeating this process up to the top
level All-Projects.
@@ -56,7 +56,7 @@ deleted afterwards.
* prolog-shell: Simple command line Prolog interpreter
+
Define a small interactive interpreter that users or site
-administartors can play around with by downloading the Gerrit WAR
+administrators can play around with by downloading the Gerrit WAR
file and executing: java -jar gerrit.war prolog-shell
Prolog Predicates
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
new file mode 100644
index 0000000000..ba4e204b27
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -0,0 +1,94 @@
+Release notes for Gerrit 2.5.1
+==============================
+
+Gerrit 2.5.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war]
+
+There are no schema changes from 2.5, or 2.5.1.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Security Fixes
+--------------
+* Correctly identify Git-over-HTTP operations
++
+Git operations over HTTP should be classified as using AccessPath.GIT
+and not WEB_UI. This ensures RefControl will correctly test for Create,
+Push or Delete access on a reference instead of Owner.
++
+E.g. without this fix project owners are able to force push commits
+via HTTP that are already in the history of the target branch, even
+without having any Push access right assigned.
+
+* Make sure only Gerrit admins can change the parent of a project
++
+Only Gerrit administrators should be able to change the parent of a
+project because by changing the parent project access rights and BLOCK
+rules which are configured on a parent project can be avoided.
++
+The `set-project-parent` SSH command already verifies that the caller
+is a Gerrit administrator, however project owners can change the parent
+project by modifying the `project.config` file and pushing to the
+`refs/meta/config` branch.
++
+This fix ensures that changes to the `project.config` file that change
+the parent project can only be pushed/submitted by Gerrit
+administrators.
++
+In addition it is now no longer possible to
+- set a non-existing project as parent (as this would make the project
+ be orphaned)
+- set a parent project for the `All-Projects` root project (the root
+ project by definition has no parent)
+by pushing changes of the `project.config` file to `refs/meta/config`.
+
+Bug Fixes
+---------
+* Fix RequestCleanup bug with Git over HTTP
++
+Decide if a continuation is going to be used early, before the filter
+that will attempt to cleanup a RequestCleanup. If so don't allow
+entering the RequestCleanup part of the system until the request is
+actually going to be processed.
++
+This fixes the IllegalStateException `Request has already been cleaned
+up` that occurred when running on Jetty and pushing over HTTP for URLs
+where the path starts with `/p/`.
+
+* Match all git fetch/clone/push commands to the command executor
++
+Route not just `/p/` but any Git access to the same thread pool as the
+SSH server is using, allowing all requests to compete fairly for
+resources.
+
+* Fix auto closing of changes on direct push
++
+When a commit is directly pushed into a repository (bypassing code
+review) and this commit has a Change-Id in its commit message then the
+corresponding change is automatically closed if it is open.
+
+* Allow assigning `Push` for `refs/meta/config` on `All-Projects`
++
+The `refs/meta/config` branch of the `All-Projects project` should only
+be modified by Gerrit administrators because being able to do
+modifications on this branch means that the user could assign himself
+administrator permissions.
++
+In addition to being administrator we already require that the
+administrator has the `Push` access right for `refs/meta/config` in
+order to be able to modify it (just as with all other branches
+administrators do not have edit permissions by default).
++
+The problem was that assigning the `Push` access right for
+`refs/meta/config` on the `All-Projects` project was not allowed.
++
+Having the `Push` access right for `refs/meta/config` on the
+`All-Projects` project without being administrator already has no
+effect.
++
+Prohibiting to assign the Push access right for `refs/meta/config` on
+the `All-Project` project was anyway pointless since it was e.g.
+possible to assign the `Push` access right on `refs/meta/*`.
+
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
new file mode 100644
index 0000000000..ceb23c280b
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -0,0 +1,138 @@
+Release notes for Gerrit 2.5.2
+==============================
+
+Gerrit 2.5.2 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war]
+
+There are no schema changes from 2.5, or 2.5.1.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Bug Fixes
+---------
+* Improve performance of ReceiveCommits for repos with many refs
++
+When validating the received commits all existing refs were added as
+uninteresting to the RevWalk. This resulted in bad performance when a
+repository had many refs (>100000). Putting existing 'refs/changes/'
+or 'refs/tags/' into the RevWalk is now avoided, which improves the
+performance.
+
+* Improve Push performance by discarding 'cache-automerge/*' refs
+ early in VisibleRefFilter
++
+For a typical large Git repository, with many refs and lots of cached
+merges, the push time goes down significantly.
+
+* Don't display all files from a merge-commit when auto-merge fails
++
+For merge commits Gerrit shows the difference to the automatic merge
+result. The creation of the auto-merge result may fail, e.g. when the
+merge commit has multiple merge bases (because JGit doesn't support
+this case yet). In this case Gerrit was showing all files from the
+merge commit. This caused several issues:
++
+--
+** the file list was too large for projects with a large number of
+ files
+** Gerrit would send too many false notification emails to users
+ watching changes under certain paths
+** both client and server needed a lot of resources in order to handle
+ such a large list of files
+--
++
+Now the file list for a merge commit will be empty when the creation
+of the auto-merge result fails.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1726[issue 1726]:
+ Create ref for new patch set on direct push
++
+If a change is in review and a new commit that has the Change-Id of
+this change in its commit message is pushed directly, then a new patch
+set for this commit is created and the change gets automatically
+closed. The problem was that no change ref for this new patch set was
+created and as result the change ref that was shown for the new patch
+set in the WebUI, and which was contained in the patchset-created
+event, was invalid.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1767[issue 1767]:
+ Remove wrong error message when pushing a new ref fails
++
+If pushing a new ref was rejected because the user was not allowed to
+create it the error message always told the user that he's missing the
+'Create Reference' access right. This message was incorrect in some
+cases. Users that have the 'Create Reference' access right assigned
+are e.g. not allowed to create the ref if:
++
+--
+** they are pushing an annotated tag without having the
+ 'Push Annotated Tag' access right
+** they are pushing a signed tag without having the 'Push Signed Tag'
+ access right
+** the project state is set to 'Read Only'
+--
++
+Now the error message just says 'Prohibited by Gerrit'. This generic
+error message is better than a more concrete error message which is
+wrong in same cases because a wrong message is misleading and
+confuses the user.
++
+In addition the description of the 'Prohibited by Gerrit' error in the
+documentation has been updated to explain some additional cases in
+which the 'Prohibited by Gerrit' error occurs.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1444[issue 1444]:
+ Remove 'Mailing-List' header from sent emails
++
+The non-standard 'Mailing-List' header that is included in the emails
+sent by Gerrit isn't allowed by the Amazon Simple Email Service and is
+now removed.
+
+* Improve SMTP client error messages
++
+The wording of the error messages in the SMTP client was changed to
+make it more clear at exactly what stage in the SMTP transaction the
+server returned an error. Also the server's response text is now
+always included.
++
+In addition it is now ensured that already rejected recipients are
+included in the error message when the server rejects the DATA
+command. Without this there is no way of debugging rejected
+recipients if all recipients are rejected since that typically
+results in a DATA command rejection. Because some SMTP servers (e.g.
+Postfix with the default configuration) delay rejection of HELO/EHLO
+and MAIL FROM commands to the RCPT TO stage, this can happen not only
+for bad recipients.
+
+* Allow time unit variables to be '0'
++
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html[
+Gerrit Configuration parameters] that expect a numerical time unit as
+value can now be set to '0'.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1076[issue 1076]:
+ Fix CLA hyperlink on account registration page
++
+The New Contributor Agreement hyperlink on the Account Registration page
+was malformed.
+
+* Fix broken link to repo command reference
++
+The link to the repo command reference in the 'repo upload' section of
+the 'Uploading Changes' documentation was broken.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1569[issue 1569]:
+Fix unexpected behaviour in the commit-msg hook caused by `GREP_OPTIONS`
++
+If `GREP_OPTIONS` was set, it caused unexpected behaviour in the
+commit-msg hook. For example if it included a setting like
+`--exclude=".git/*"` it caused a new `Change-Id` line to be appended
+to the commit message on every amend.
++
+`GREP_OPTIONS` is now unset at the beginning of the commit-msg script
+to prevent such problems from occurring.
++
+The `GREP_OPTIONS` setting in the user's environment is unaffected
+by this change.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
new file mode 100644
index 0000000000..60efa7a3a1
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -0,0 +1,22 @@
+Release notes for Gerrit 2.5.3
+==============================
+
+Gerrit 2.5.3 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war]
+
+There are no schema changes from any of the 2.5.x versions.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Security Fixes
+--------------
+* Patch vulnerabilities in OpenID client library
++
+Installations using OpenID for authentication were vulnerable to a
+number of attacks over the network. The openid4java client library
+was identified as the entry point. In this release Gerrit updated to
+the latest 0.9.8 release, which patches the known attack vectors.
+
+No other changes since 2.5.2.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.4.txt b/ReleaseNotes/ReleaseNotes-2.5.4.txt
new file mode 100644
index 0000000000..1657d9b4fc
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.4.txt
@@ -0,0 +1,22 @@
+Release notes for Gerrit 2.5.4
+==============================
+
+Gerrit 2.5.4 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war]
+
+There are no schema changes from any of the 2.5.x versions.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Bug Fixes
+---------
+* Require preferred email to be verified
++
+Some users were able to select a preferred email address that was
+not previously verified. This may have allowed the server to send
+notifications to an invalid destination, resulting in higher than
+usual bounce rates.
+
+No other changes since 2.5.3.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
new file mode 100644
index 0000000000..55fbb60d6e
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -0,0 +1,1950 @@
+Release notes for Gerrit 2.5
+============================
+
+Gerrit 2.5 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war]
+
+Gerrit 2.5 includes the bug fixes done with
+link:ReleaseNotes-2.4.1.html[Gerrit 2.4.1] and
+link:ReleaseNotes-2.4.2.html[Gerrit 2.4.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.5.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.5.x. If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.5.x.
+
+Warning on upgrade to schema version 68
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The migration to schema version 68, may result in a warning, which can
+be ignored when running init in the interactive mode.
+
+E.g. this warning may look like this:
+
+----
+Upgrading database schema from version 67 to 68 ...
+warning: Cannot create index for submodule subscriptions
+Duplicate key name 'submodule_subscriptions_access_bySubscription'
+Ignore warning and proceed with schema upgrade [y/N]?
+----
+
+This migration is creating an index for the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-submodules.html[submodule feature] in
+Gerrit. When the submodule feature was introduced the index was only
+created when a new site was initialized, but not when Gerrit was
+upgraded. This migration tries to create the index, but it will only
+succeed if the index does not exist yet. If the index exists already,
+the creation of the index will fail. There was no database independent
+way to detect this case and this is why this migration leaves it to the
+user to decide if a failure should be ignored or not. If from the error
+message you can see that the migration failed because the index exists
+already (as in the example above), you can safely ignore this warning.
+
+Upgrade Warnings
+----------------
+
+[[replication]]
+Replication
+~~~~~~~~~~~
+
+Gerrit 2.5 no longer includes replication support out of the box.
+Servers that reply upon `replication.config` to copy Git repository
+data to other locations must also install the replication plugin.
+
+Cache Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Disk caches are now backed by individual H2 databases, rather than
+Ehcache's own private format. Administrators are encouraged to clear
+the `'$site_path'/cache` directory before starting the new server.
+
+The `cache.NAME.diskLimit` configuration variable is now expressed in
+bytes of disk used. This is a change from previous versions of Gerrit,
+which expressed the limit as the number of entries rather than bytes.
+Bytes of disk is a more accurate way to size what is held. Admins that
+set this variable must update their configurations, as the old values
+are too small. For example a setting of `diskLimit = 65535` will only
+store 64 KiB worth of data on disk and can no longer hold 65,000 patch
+sets. It is recommended to delete the diskLimit variable (if set) and
+rely on the built-in default of `128m`.
+
+The `cache.diff.memoryLimit` and `cache.diff_intraline.memoryLimit`
+configuration variables are now expressed in bytes of memory used,
+rather than number of entries in the cache. This is a change from
+previous versions of Gerrit and gives administrators more control over
+how memory is partioned within a server. Admins that set this variable
+must update their configurations, as the old values are too small.
+For example a setting of `memoryLimit = 1024` now means only 1 KiB of
+data (which may not even hold 1 patch set), not 1024 patch sets. It
+is recommended to set these to `10m` for 10 MiB of memory, and
+increase as necessary.
+
+The `cache.NAME.maxAge` variable now means the maximum amount of time
+that can elapse between reads of the source data into the cache, no
+matter how often it is being accessed. In prior versions it meant how
+long an item could be held without being requested by a client before
+it was discarded. The new meaning of elapsed time before consulting
+the source data is more useful, as it enables a strict bound on how
+stale the cached data can be. This is especially useful for slave
+servers account and permission data, or the `ldap_groups` cache, where
+updates are often made to the source without telling Gerrit to reload
+the cache.
+
+New Features
+------------
+
+Plugins
+~~~~~~~
+
+The Gerrit server functionality can be extended by installing plugins.
+Depending on how tightly the extension code is coupled with the Gerrit
+server code, there is a distinction between
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#plugin[plugins] and
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#extension[extensions].
+
+* link:#replication[Move replication logic to replication plugin]
++
+This splits all of the replication code out of the core server
+and moves it into a standard plugin.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html[Documentation about
+ plugin development] including instructions for:
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#getting-started[how to get
+ started with plugin development]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#deployment[plugin
+ deployment/installation]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#API[API for plugins and
+ extensions]
+
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#ssh[SSH command
+ plugins]
++
+Allows plugin developers to declare additional SSH commands.
+
+* Enable link:#ssh-alias[aliases for SSH commands]
++
+Site administrators can alias SSH commands from a plugin into the
+`gerrit` namespace.
++
+The aliases are configured statically at server startup, but are
+resolved dynamically at invocation time to the currently loaded
+version of the plugin. If the plugin is not loaded, or does not
+define the command, "not found" is returned to the user.
+
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#http[HTTP
+ plugins]
++
+Plugins may contribute to the /plugins/NAME/ URL space.
+
+* Automatic registration of plugin bindings
++
+If a plugin has no modules declared in the manifest, automatically
+generate the modules for the plugin based on the class files that
+appear in the plugin and the `@Export` annotations that appear on
+these concrete classes.
++
+For any non-abstract command that extends SshCommand, plugins may
+declare the command with `@Export("name")` to
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#ssh[bind the implementation
+as that SSH command].
++
+Likewise link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#http[HTTP servlets
+can also be bound to URLs].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#data-directory[Support a data
+ directory for plugins on demand]
+
+* Support serving static/ and Documentation/ from plugins
++
+The static/ and Documentation/ resource directories of a plugin can be
+served over HTTP for any loaded and running plugin, even if it has no
+other HTTP handlers. This permits a plugin to supply icons or other
+graphics for the web UI, or documentation content to help users learn
+how to use the plugin.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#documentation[Auto-formatting
+ of plugin HTTP pages from Markdown files]
++
+If Gerrit detects that a requested plugin resource does not exist, but
+instead a file with a `.md` extension does exist, Gerrit opens the
+`.md` file and reformats it as html.
+
+* Support of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#macros[macros in
+ Markdown plugin documentation]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#auto-index[Automatic
+ generation of an index for the plugin documentation]
+
+* Support for audit plugins
++
+Plugins can implement an `AuditListener` to be informed about auditable
+actions:
++
+----
+ @Listener
+ public class MyAuditTrail extends AuditListener
+----
++
+The plugin must define a plugin module that binds the implementation of
+the audit listener in the `configure()` method:
++
+----
+ DynamicSet.bind(binder(), AuditListener.class).to(MyAuditTrail.class);
+----
+
+* Web UI for plugins
++
+Administrators can see the list of installed plugins in the WebUI
+under `Admin` > `Plugins`. For each plugin the plugin status is shown
+and it is possible to navigate to the plugin documentation.
+
+* Servlet to list plugins
++
+Administrators can retrieve plugin information from a REST interface
+by loading `<server-url>/a/plugins/`.
+
+* Support SSH commands to
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-ls.html[list the installed
+ plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-install.html[install plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-enable.html[enable plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-remove.html[disable plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-reload.html[reload plugins]
+
+* Support installation of core plugin on site initialization
+
+* Automatically load/unload/reload plugins
++
+The PluginScanner thread runs every 1 minute by default and loads any
+newly created plugins, unloads any deleted plugins, and reloads any
+plugins that have been modified.
++
+The check frequency can be configured by setting
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#plugins.checkFrequency[
+plugins.checkFrequency] in the Gerrit config file. By configuration
+the scanner can also be disabled.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#classpath[Loading of plugins
+ in own ClassLoader]
+
+* Plugin cleanup in the background
++
+When a plugin is stopped, schedule a Plugin Cleaner task to run
+1 minute later to try and clean out the garbage and release the
+JAR from `$site_path/tmp`.
+
+* Export `LifecycleListener` as extension point
++
+Extensions may need to know when they are starting or stopping.
+Export the interface that they can use to learn this information.
+
+* Support injection of `ServerInformation` into extensions and plugins
++
+Plugins can take this value by injection and learn the current
+server state during their own LifecycleListener. This enables a
+plugin to determine if it is loading as part of server startup, or
+because it was dynamically installed or reloaded by an administrator.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#getting-started[Maven
+ archetype for creating gerrit plugin projects]
+
+* Enables the use of session management in Jetty
++
+This enables plugins to make use of servlet sessions.
+
+REST API
+~~~~~~~~
+Gerrit now supports a REST like API available over HTTP. The API is
+suitable for automated tools to build upon, as well as supporting some
+ad-hoc scripting use cases.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html[Documentation of the REST API]
+
+* Support REST endpoints to
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[query changes]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#projects[list projects]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#suggest-projects[suggest
+ projects]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#accounts_self_capabilities[query
+ the global capabilities of the calling user]
+
+* Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#authentication[anonymous
+ and authenticated access] to the REST endpoints
+
+* Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#output[JSON output
+ format] for the REST endpoints
+
+The new REST API is used from the Gerrit WebUI.
+
+Some of the methods from the old internal JSON-RPC interface were
+completely replaced by the new REST API and got deleted:
+
+* `ProjectAdminService.visibleProjects(AsyncCallback<ProjectList>)`
+* `ProjectAdminService.suggestParentCandidates(AsyncCallback<List<Project>>)`
+* `ChangeListService.myStarredChangeIds(AsyncCallback<Set<Change.Id>>)`
+* `ChangeListService.allQueryNext(String, String, int, AsyncCallback<SingleListChangeInfo>)`
+* `ChangeListService.allQueryPrev(String, String, int, AsyncCallback<SingleListChangeInfo>)`
+* `ChangeListService.forAccount(Account.Id, AsyncCallback<AccountDashboardInfo>)`
+
+[[query-deprecation]]
+In addition the `/query` API has been deprecated. By default it is
+still available but server administrators may disable it by setting
+the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#site.enableDeprecatedQuery[
+`site.enableDeprecatedQuery`] parameter in the Gerrit config file. This
+allows to enforce tools to move to the new API.
+
+Web
+~~~
+
+Change Screen
+^^^^^^^^^^^^^
+
+* Display commit message in a box
++
+The commit message on the change screen is now placed in a box with a
+title and emphasis on the commit summary. The star icon and the
+permalink are displayed in the box header. The header from the change
+screen is removed as it only held duplicate information.
+
+* Open the dependency section automatically when the change is needed
+ by an open change
+
+* Only show a change as needed by if its current patch set depends on
+ the change
+
+* Show only changes of the same project in the 'Depends On' section
++
+If two projects share the same history it can happen that the same
+commit is pushed for both projects, resulting in two changes. If now
+a successor commit is pushed for one of the projects, the resulting
+successor change was wrongly listing both changes in the 'Depends On'
+section. Now only the predecessor change of the own project is listed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1383[issue 1383]:
+ Display the approval table on the PublishCommentsScreen.
++
+So far the approval table that shows the reviewers and their current
+votes was only shown on the ChangeScreen. Now it is also shown on the
+PublishCommentScreen. This allows the reviewer to see all existing
+votes and reviewers when doing their own voting and publishing of
+comments. Seeing the existing votes helps the reviewer in
+understanding which votes are still required before the change can be
+submitted.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1380[issue 1380]:
+ Display time next to change comments
++
+When a comment was posted yesterday, or any time older than 1 day but
+less than 1 year ago, display the time too. Display "May 2 17:37" rather
+than just "May 2".
+
+* Only show "Can Merge" when the change is new or draft
+
+* Allow auto suggesting reviewers to draft changes
++
+Auto completing users for draft changes did't work as the other
+users didn't have access to the drafts. The visibility check for
+the reviewer suggestion is now skipped.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1294[issue 1294]:
+ Shorten subject of parent commit for displaying in the UI
++
+If the parent commit has a very long subject (> 80 characters) shorten
+the subject for displaying it in the Gerrit web UI on the change screen.
+This avoids that the 'Parent(s)' cell for the patch set becomes very
+wide.
+
+* If subject is shortened for displaying in the UI indicate this by '...'
++
+If a commit has a very long subject line (> 80 characters) it is
+shortened when it is displayed in the Gerrit Web UI. Indicate to the
+user that the subject was shortened by appending '...' to the shortened
+subject.
++
+Also the subject is now cropped after a whitespace if possible.
+
+* Insert Change-Id for revert commits
++
+The 'Revert Change' action on a merged change allows to create a new
+change that reverts the merged change. The commit message of the revert
+commit now contains a Change-Id.
++
+It is convenient if a Change-Id is automatically created and inserted
+into the commit message of the revert commit since it makes rebasing of
+the revert commit easier.
+
+* Use more gentle shade of red to highlight outdated dependencies
+
+Patch Screens
+^^^^^^^^^^^^^
+
+* New patch screen header
++
+A new patch screen header was added that is displayed above both the
+side-by-side and unified views. The new header contains actual links to
+the available patchsets and shows which patchset is being currently
+displayed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1192[issue 1192]:
+ Add download links to the unified diff view
+
+* Improvement of the side-by-side viewer table
++
+The line number column for the right side was moved to be on the far
+right of the table, so that the layout now looks like this:
++
+----
+ 1 | foo | bar | 1
+ 2 | hello | hello | 2
+----
++
+This looks nicer when reading a lot of code, as the line numbers are
+less relevant than the code itself which is now in the center of the
+UI.
++
+Line numbers are still links to create comment editors, but they
+use a light shade of gray and skip the underline decoration, making
+them less visually distracting.
++
+Skip lines now use a paler shade of blue and also hide the fact they
+contain anchors, until you hover over them and the anchor shows up.
++
+The expand before and after are changed to be arrows showing in
+which direction the lines will appear above or below the skip
+line.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=626[issue 626]:
+ Option to display line endings
++
+There is a new user preference that allows to display Windows EOL/Cr-Lf.
+'\r' is shown in a dotted-line box (similar to how '\r' is displayed in
+GitWeb).
+
+* Streamlined review workflow
++
+A link was added next to the "Reviewed" checkbox that marks the current
+patch as reviewed and goes to the next unreviewed patch.
+
+* Add key commands to mark a patch as reviewed
++
+Add key commands
++
+. to toggle the reviewed flag for a patch ('m')
++
+and
++
+. to mark the patch as reviewed and navigate to the next unreviewed
+patch ('M').
+
+* Use download icons instead of the `Download` text links
+
+User Dashboard
+^^^^^^^^^^^^^^
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-custom-dashboards.html[custom
+ dashboards]
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1407[issue 1407]:
+ Improve highlighting of unreviewed changes in the user's dashboard
++
+A change will be highlighted as unreviewed if
++
+. the user is reviewer of the change but hasn't published any change
+ message for the current patch set
+. the user has published a change message for the current patch set,
+ but afterwards the change owner has published a change message on
+ the change
+
+* Sort outgoing reviews in the user dashboard by created date
+
+* Sort incoming reviews in the user dashboard by updated date
++
+Sorting the incoming reviews by last updated date, descending, places
+the most recently updated reviews at the top of the list for a user,
+and the oldest stale at the bottom. This may help users to identify
+items to take immediate action on, as they appear closer to the top.
+
+Access Rights Screen
+^^^^^^^^^^^^^^^^^^^^
+
+* Display error if modifying access rights for a ref is forbidden
++
+If a user is owner of at least one ref he is able to edit the access
+rights on a project. If he adds access rights for other refs, these
+access rights were silently ignored on save. Instead of this now an
+error message is displayed to inform the user that he doesn't have
+permissions to do the update for these refs.
++
+In case of such an error the project access screen stays in the edit
+mode so that the unsaved modifications are not lost. The user may now
+propose the changes to the access rights through code review.
+
+* Allow to propose changes to access rights through code review
++
+Users that are able to upload changes for code review for the
+`refs/meta/config` branch can now propose changes to the project access
+rights through code review directly from the ProjectAccessScreen.
++
+When editing the project access rights there is a new button
+'Save for Review' which will create a new change for the access
+rights modifications. Project owners are automatically added as
+reviewer to this change. If a project owner agrees to the access rights
+modifications he can simply approve and submit the change.
+
+* Show all access rights in WebUI if user can read `refs/meta/config`
++
+Users who can read the `refs/meta/config` branch, can see all access
+rights by fetching this branch and looking at the `project.config`
+file. Now they can see the same information in the web UI.
+
+* Allow extra group suggestions for project owners
++
+When suggesting groups to a user, only groups that are visible to the
+user are suggested. These are those group that the user is member of.
+For project owners now also groups to which they are not a member are
+suggested when editing the access rights of the project.
+
+Other
+^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1592[issue 1592]:
+ Ask user to login if change is not found
++
+Accessing a change URL was failing with 'Application Error - The page
+you requested was not found, or you do not have permission to view this
+page' if the user was not signed in and the change was not visible to
+`Anonymous Users`. Instead Gerrit now asks the user to login and
+afterwards shows the change to the user if it exists and is visible.
+If the change doesn't exist or is not visible, the user will still get
+the NotFoundScreen after sign in.
+
+* Link to owner query from user names
++
+Instead of linking from a user name to the user's dashboards, link to
+a search for changes owned by that user.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#gerrit.reportBugUrl[Allow
+ configuring the `Report Bug` URL]
++
+Let site administrators direct users to their own ticket queue, as for
+many servers most of the reported bugs are small internal problems like
+asking for a repository to be created or updating group memberships.
+
+* On project creation allow choosing the parent project from a popup
++
+In the create project UI a user can now browse all projects and select
+one as parent for the new project.
+
+* Check for open changes on branch deletion
++
+Check for open changes when deleting a branch in the Gerrit WebUI.
+Delete a branch only if there are no open changes for this branch.
+This makes users aware of open changes when deleting a branch.
+
+* Enable ProjectBranchesScreen for the `All-Projects` project
++
+This allows to see the branches of the `All-Projects` project in the
+web UI.
+
+* Show for each project in the project list a link to the repository
+ browser (e.g. GitWeb).
+
+* Move the project listing menu items to a new top-level item
++
+Finding the project listing was very opaque to end users. Nobody
+expected to look under `Admin` and furthermore, anonymous users were
+unable to find that link at all.
++
+Introduced a new top-level `Projects` menu that has `List` in it to
+take you to the project listing.
++
+In addition the `Create new project` link from the top of that listing
+was moved to this new menu.
+
+* Move the Groups and Plugins menu items to the top level
++
+The top-level Admin menu is removed as it is now unnecessary after the
+Projects, Groups and Plugins menu items were moved to the top-level.
+
+* Move form for group creation to own screen
++
+Move the form for the group creation from the GroupListScreen to an
+own new CreateGroupScreen and add a link to this screen at the
+beginning of the GroupListScreen. The link to the CreateGroupScreen is
+only visible if the user has the permission to create new groups.
+
+* Drop the `Owners` column from the group list screen
++
+The `Owners` column on the group list screen has been dropped in order
+to link:#performance-issue-on-showing-group-list[speed up the loading
+of the group list screen].
+
+* Drop the `Group Type` column from the group list screen
++
+Since link:#migrate-ldap-groups[the LDAP group type was removed] there
+is no need to display the group type on the group list screen anymore.
+There are only 3 `SYSTEM` groups using well known names, and everything
+else has the type `INTERNAL`.
+
+* When adding a user to a group create an account for the user if needed
++
+Trying to add a user to a group that doesn't have an account fails with
+'... is not a registered user.'. Now adding a user to a group does not
+immediately fail if there is no account for the user, but it tries to
+authenticate the user and if the authentication is successful a user
+account is automatically created, so that the user can be added to the
+group. This only works if LDAP is used as user backend.
++
+This allows to add users to groups that did not log in into Gerrit
+before.
+
+* Differentiate between draft changes and draft comments
++
+Show the draft changes of the user when he clicks on `My` > `Drafts`.
+The user's draft comments are now available under `My` >
+`Draft Comments`.
+
+* Show NotFoundScreen if a user that can't create projects tries to
+ access the ProjectCreationScreen
+
+* Add Edit, Reload next to non-editable Full Name field
++
+If the user database is actually an external system users might need go
+to another server to edit their account data, and then re-import their
+account data by going through a login cycle. This is highly similar to
+LDAP where the directory provides account data and its refreshed every
+time the user visits the `/login/` URL handler.
++
+The URL for the external system can be configured for the
+link:#custom-extension[`CUSTOM_EXTENSION`] auth type.
+
+Access Rights
+~~~~~~~~~~~~~
+
+* Restrict rebasing of a change in the web UI to the change owner and
+ the submitter
+
+* Add a new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_rebase[
+ access right to permit rebasing changes in the web UI]
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=930[issue 930]:
+ Add new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_abandon[
+ access right for abandoning changes]
+
+* Check if user can upload in order to restore
++
+Restoring a change is similar to uploading a new change. If a branch
+gets closed by removing the access rights to upload new changes it
+shouldn't be possible to restore changes for this branch.
+
+[[hide-config]]
+* Make read access to `refs/meta/config` by default exclusive to
+ project owners
++
+When initializing a new site a set of default access rights is
+configured on the `All-Projects` project. These default access rights
+include read access on `refs/*` for `Anonymous Users` and read access
+on `refs/meta/config` for `Project Owners`. Since the read access on
+`refs/meta/config` for `Project Owners` was not exclusive,
+`Anonymous users` were able to access the `refs/meta/config` branch
+which by default should only be accessible by the project owners.
+
+Search
+~~~~~~
+* Offer suggestions for the search operators in the search panel
++
+There are many search operators and it's difficult to remember all of
+them. Now the search operators are suggested as the user types the
+query.
+
+* Support alias `self` in queries
++
+Writing an expression like "owner:self status:open" will now identify
+changes that the caller owns and are still open. This `self` alias
+is valid in contexts where a user is expected as an argument to a
+query operator.
+
+* Add parent(s) revision information to output of query command
+
+* Add owner username to output of query command
+
+* `/query` API has been link:#query-deprecation[deprecated]
+
+SSH
+~~~
+* link:http://code.google.com/p/gerrit/issues/detail?id=1095[issue 1095]
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-account.html[SSH command to manage
+ accounts]
+
+* On link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-account.html[account creation] a
+ password for HTTP can be specified.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-project.html[SSH command to manage
+ project settings]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-test-submit-rule.html[SSH command to test
+ submit rules]
++
+The command creates a fresh Prolog environment and loads a Prolog
+script from stdin. `can_submit` is then queried and the results are
+returned to the user.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ban-commit.html[SSH command to ban
+ commits]
+
+[[ssh-alias]]
+* Enable aliases for SSH commands
++
+Site administrators can define aliases for SSH commands in the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#ssh-alias[`ssh-alias` section]
+of the Gerrit configuration.
+
+* Add submit records to the output of the
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-query.html[query] SSH command:
++
+Add a command line option to the `query` SSH command to include submit
+records in the output.
++
+This facilitates the querying of information relating to the submit
+status from the command line and by API clients, including information
+such as whether the change can be submitted as-is, and whether the
+submission criteria for each review label has been met.
+
+* Support JSON output format for the
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ls-projects.html[ls-projects] SSH command
+
+* Support creation of multiple branches in
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-project.html[create-project] SSH
+ command
++
+In case if a project has some kind of waterfall automerging
+a->b->c it is convenient to create all these branches at the
+project creation time.
++
+e.g. '.. gerrit create-project -b master -b foo -b bar ...'
+
+* Add verbose output option to
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ls-groups.html[ls-groups] command
++
+The verbose mode enabled by the new option makes the ls-groups
+command output a tab-separated table containing all available
+information about each group (though not its members).
+
+Documentation
+~~~~~~~~~~~~~
+
+Commands
+^^^^^^^^
+
+* document for the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-group.html[`create-group`]
+ command that for unknown users an account is automatically created if
+ the LDAP authentication succeeds
+
+* Update documentation and help text for the
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-review.html[`review`] SSH command
++
+The review command can be applied to multiple changes, but the
+help text was written in singular tense.
++
+Add a paragraph in the documentation explaining that the
+`--force-message` option will not be effective if the `review` command
+fails because the user is not permitted to change the label.
+
+* Clarify that `init --batch` doesn't drop old database objects
+
+* Update the list of unsupported slave commands
+
+* Fix link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-stream-events.html[`stream-events`]
+ documentation
++
+Some attributes contained in the events were not described, for a few
+others the name was given in a wrong case.
+
+* Fix and complete synopsis of commands
+
+Access Control
+^^^^^^^^^^^^^^
+
+* Clarify the ref format for
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_push_merge[`Push
+ Merge Commit`]
++
+Elaborate on the required format of the ref used for `Push Merge Commit`
+access right entries to avoid user confusion when granting access to
+`refs/heads/*` still doesn't allow them to push any merge commits.
+
+* Document the
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#capability_emailReviewers[
+ `emailReviewers`] capability
+
+Error
+^^^^^
+* Improve documentation of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/error-change-closed.html[
+ `change closed` error]
++
+The `change closed` error can also occur when trying to submit a
+review label with the SSH review command onto a change that has
+been closed (submitted and merged, or abandoned) or onto a patchset
+that has been replaced by a newer patchset.
+
+* Correct documentation of `invalid author` and `invalid committer`
+ errors
++
+The error messages `you are not committer ...` and `you are not
+author ...` were replaced with `invalid author` and `invalid
+committer`.
+
+* Describe that the `prohibited by Gerrit` error is returned if pushing
+ a tag fails because the tagger is somebody else and the `Forge
+ Committer` access right is not assigned.
+
+Dev
+^^^
+
+* Update push URL in link:../SUBMITTING_PATCHES[SUBMITTING_PATCHES]
++
+Pushes are now accepted at the same address as clone/fetch/pull.
+
+* Update link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-contributing.html[contributor
+ document]
++
+We now prefer to use Guava (previously known as Google Collections).
+
+* Fixed broken link to source code
++
+Updated the documentation source code links to point to:
+http://code.google.com/p/gerrit/source/checkout
+
+* State link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-eclipse.html#known-problems[known issues]
+ when debugging Gerrit with Eclipse
+
+* Improved the section on
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-eclipse.html#hosted-mode[hosted mode
+ debugging]
++
+The existing section on hosted mode debugging left out a couple of
+steps, and the requirement to use `DEVELOPMENT_BECOME_ANY_ACCOUNT`
+instead of `OpenID` was not mentioned anywhere.
+
+* Add a link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html[release preparation
+ document]
++
+Document what it takes to make a Gerrit stable or stable-fix release,
+and how to release Gerrit subprojects.
+
+Other
+^^^^^
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/prolog-cookbook.html[Cookbook for Prolog
+ submit rules]
++
+A new document providing a step by step introduction into implementing
+specific submit policies using Prolog based submit rules was added.
+
+* Describe link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/refs-notes-review.html[
+ `refs/notes/review` and its contents]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-mail.html[Document `RebasedPatchSet.vm`
+ and `Reverted.vm` mail templates]
+
+* Specify output file for curl commands in documentation
++
+For downloading the `commit-msg` hook and the `gerrit-cherry-pick`
+script users can either use scp or curl. Specify the output file for
+each curl command so that the result is equal to the matching scp
+command.
+
+* Document that user must be in repository root to install `commit-msg`
+ hook
+
+* Add some clarifications to the
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/install-quick.html[quick installation guide]
+
+* Add missing documentation about
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#hooks[hook configuration]
++
+Add documentation of hook config for `change-restored`, `ref-updated`
+and `cla-signed` hooks.
+
+* Document that the commit message hook file should be executable
+
+* Mention that also MySQL supports replication, not just Postgres
+
+* Make sorting of release notes consistent so that the release notes
+ for the newest release is always on top
+
+* Various corrections
++
+Correct typos, spelling mistakes, and grammatical errors.
+
+Dev
+~~~
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html#plugin-api[script for
+ releasing plugin API jars]
+
+* Pushes are now accepted at the same address as clone/fetch/pull
++
+To submit patches commits can be pushed to
+https://gerrit.googlesource.com/gerrit
+
+* Add `-Pchrome`, `-Pwebkit`, `-Pfirefox` aliases for building
++
+This makes it easier to build for the browser you want to
+test on, rather than remembering what its GWT name is.
+
+* Disable assertions for KeyCommandSet when running in gwtdebug mode
++
+The assertions in the KeyCommandSet class cause exceptions when a
+KeyCommand is registered several times.
+
+* Add the run profiles to the favorites menu
+
+* Add Intellij IDEA files to ignore list
+
+* Move local Maven repository to Google Cloud Storage
+
+* Make sure asciidoc uses unix line endings in generated HTML.
++
+Use an explicit asciidoc attribute to make sure the produced HTML will
+always contain unix line endings. This will help in producing build
+results that are better comparable by size.
+
+* Remove timestamp from all `org.eclipse.core.resources.prefs` files
++
+Eclipse overwrites these files when we import projects using m2e.
+Eclipse 3 writes a timestamp at the top of these files making the Git
+working tree dirty. Eclipse 4 (Juno) still overwrites these files but
+doesn't write the timestamp. This should help to keep the working tree
+clean. However, since the timestamp is currently present in these
+files, Eclispe 4 would still make them dirty by overwriting and
+effectively removing the timestamp.
++
+This change removes the timestamp from these files. This helps those
+using Eclipse 4 and doesn't make it worse for those still using Eclispe
+3.
+
+* Add Maven profile to skip build of plugin modules
++
+Building the plugin modules ('Plugin API' and 'Plugin Archetype') may
+take a significant amount of time (since many jars are downloaded).
+During development it is not needed to build the plugin modules. A new
+Maven profile was added that skips the build of the plugin modules,
+so that developers have a faster turnaround. This profile is called
+`no-plugins` and it's active by default. To include the plugin modules
+into the build activate the `all` profile:
++
+----
+ mvn clean package -P all
+----
++
+The script to make release builds has been adapted to activate the
+`all` profile so that the plugin modules are always built for release
+builds.
+
+Mail
+~~~~
+
+* Add unified diff to newchange mail template
++
+Add `$email.UnifiedDiff` as new macro to the `NewChange.vm` mail
+template. This macro is expanded to a unified diff of the patch.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#sendemail.includeDiff[
+ sendemail.includeDiff]: Enable `$email.UnifiedDiff` in `NewChange.vm`
++
+Instead of making site administrators hack the email template, allow
+admins to enable the diff feature by setting a configuration variable
+in `gerrit.config`.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#sendemail.maximumDiffSize[
+ sendemail.maximumDiffSize]: Limit the size of diffs sent by email
++
+If a unified diff included in an email will exceed the limit configured
+by the system administrator, only the affected file paths are listed in
+the email instead. This gives interested parties some context on the
+size and scope of the change, without killing their inbox.
+
+* Catch all exceptions when emailing change update
+
+* Allow unique from address generation
++
+Allow the from email address to be a ParameterizedString that handles
+the `${userHash}` variable. The value of the variable is the md5 hash
+of the user name. This allows unique generation of email addresses, so
+GMAIL threads names of users in conversations correctly. For example,
+the from pattern for gerrit-review defined in the Gerrit configuration
+looks like this:
++
+----
+ [sendemail]
+ from = ${user} <noreply-gerritcodereview+${userHash}@google.com>
+----
+
+* Show new change URLs in the body of the new change email
++
+Some email clients hide the signature section of an email
+automatically. If there are no reviewers listed on a new change,
+such as when a change is pushed over HTTP and a notification is
+automatically sent out to any subscribed watchers, the URL was
+hidden inside of the signature and not readily available.
++
+Show the URL right away in the body.
+
+Miscellaneous
+~~~~~~~~~~~~~
+* Back in-memory caches with Guava, disk caches with H2
++
+Instead of using Ehcache for in-memory caches, use Guava. The Guava
+cache code has been more completely tested by Google in high load
+production environments, and it tends to have fewer bugs. It enables
+caches to be built at any time, rather than only at server startup.
++
+By creating a Guava cache as soon as it is declared, rather than
+during the LifecycleListener.start() for the CachePool, we can promise
+any downstream consumer of the cache that the cache is ready to
+execute requests the moment it is supplied by Guice. This fixes a
+startup ordering problem in the GroupCache and the ProjectCache, where
+code wants to use one of these caches during startup to resolve a
+group or project by name.
++
+Tracking the Gauva backend caches with a DynamicMap makes it possible
+for plugins to define their own in-memory caches using CacheModule's
+cache() function to declare the cache. It allows the core server to
+make the cache available to administrators over SSH with the gerrit
+show-caches and gerrit `flush-caches` commands.
++
+Persistent caches store in a private H2 database per cache, with a
+simple one-table schema that stores each entry in a table row as a
+pair of serialized objects (key and value). Database reads are gated
+by a BloomFilter, to reduce the number of calls made to H2 during
+cache misses. In theory less than 3% of cache misses will reach H2 and
+find nothing. Stores happen on a background thread quickly after the
+put is made to the cache, reducing the risk that a diff or web_session
+record is lost during an ungraceful shutdown.
++
+Cache databases are capped around 128M worth of stored data by running
+a prune cycle each day at 1 AM local server time. Records are removed
+from the database by ordering on the last access time, where last
+accessed is the last time the record was moved from disk to memory.
+
+* Add OpenID SSO support.
++
+Setting `OPENID_SSO` for
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.type[`auth.type`] in the
+`gerrit.config` will allow the admin to specify an SSO entry point URL
+so that users clicking on "Sign In" are sent directly to that URL.
+
+* Git over HTTP BasicAuth against Gerrit basic auth.
++
+Allows the configuration of native Gerrit username/password
+authentication scheme used for Git over HTTP BasicAuth, as alternative
+of the default DigestAuth scheme against the random generated password
+on Gerrit DB.
++
+Example setting for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.type[
+`auth.type`] and link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.gitBasicAuth[
+`auth.gitBasicAuth`]:
++
+----
+ [auth]
+ type = LDAP
+ gitBasicAuth = true
+----
++
+With this configuration Git over HTTP protocol will be authenticated
+using `HTTP-BasicAuth` and credentials checked on LDAP.
+
+* Abstract group systems into GroupBackend interface
++
+Group backends are supposed to use unique prefixes to isolate the
+namespaces. E.g. the group backend for LDAP is using `ldap/` as prefix
+for the group names.
++
+This means that to refer to an LDAP group in the WebUI the group name
+needs to be prefixed with the `ldap/` string. E.g. if there is a group
+in LDAP which is called "Developers", Gerrit will suggest this group
+when the user types `ldap/De`.
++
+WARNING: External groups are not anymore allowed to be members of
+internal groups.
+
+[[migrate-ldap-groups]]
+* Migrate existing internal LDAP groups
++
+Previously, LDAP groups were mirrored in the AccountGroup table and
+given an Id and UUID the same as internal groups. Update these groups
+to be backed by only a GroupReference, with a special "ldap:" UUID
+prefix. Migrate all existing references to the UUID in ownerGroupUUID
+and any `project.config`.
++
+This made the LDAP group type obsolete and it was removed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=548[issue 548]:
+ Make commands to download patch sets
+ link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#download.command[configurable]
++
+For patch sets on the ChangeScreen different commands for downloading
+the patch sets are offered. For some installations not all commands are
+needed. Allow Gerrit administrators to configure which download
+commands should be offered.
+
+* Add more link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#theme[theme color
+ options]
++
+** Add a theme option to change outdated background color
+** Add odd/even row background color for tables such as list of open
+reviews. This makes them more visible without clicking on them.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-notify.html[Add `notify` section in
+ `project.config`]
++
+The notify section allows project owners to include emails to users
+directly from `project.config`. This removes the need to create fake
+user accounts to always BCC a group mailing list.
+
+* Include the contributor agreements in the `project.config` and
+ migrate contributor agreements to `All-Projects`
++
+Update the parsing of `project.config` to support the contributor
+agreements.
++
+Add a new schema to move the ContributorAgreement, AccountAgreement,
+and AccountGroupAgreement information into the `All-Projects`
+`project.config`.
+
+* Add `sameGroupVisibility` to `All-Projects` `project.config`
++
+The `sameGroupVisiblity` is needed to restrict the visibility of
+accounts when `accountVisibility` is `SAME_GROUP`. Namely, this is a
+way to make sure the `autoVerify` group in a `contributor-agreements`
+section is never suggested.
+
+* Add change topic in hook arguments
++
+It was not possible for hook scripts to include topic-specific
+behaviour because the topic name was not included in the arguments.
+
+* Add `--is-draft` argument on `patchset-created` hook
++
+The `--is-draft` argument will be passed with either `true` if
+the patchset is a draft, or `false` otherwise.
++
+This can be used by hooks that need to behave differently if the
+change is a draft.
+
+* Log sign in failures on info level
++
+If for a user signing in into the Gerrit web UI fails, this can have
+many reasons, e.g. username is wrong, password is wrong, user is marked
+as inactive, user is locked in the user backend etc. In all cases the
+user just gets a generic error message 'Incorrect username or
+password.'. Gerrit administrators had trouble to find the exact reason
+for the sign in problem because the corresponding AccountException was
+not logged.
+
+* Do not log 'Object too large' as error with full stacktrace
++
+If a user pushes an object which is larger than the configured
+`receive.maxObjectSizeLimit` parameter, the push is rejected with an
+'Object too large' error. In addition an error log entry with the full
+stacktrace was written into the error log.
++
+This is not really a server error, but just a user doing something that
+is not allowed, and thus it should not be logged as error. For a Gerrit
+administrator it might still be interesting how often the limit is hit.
+This is why it makes sense to still log this on info level.
++
+For the user pushing a too large object we now do not print the
+'fatal: Unpack error, check server log' message anymore, but only the
+'Object too large' error message.
+
+* Add better explanations to rejection messages
++
+Provide information to the user why a certain push was rejected.
+
+* Automatic schema upgrade on Gerrit startup
++
+In case when Gerrit administrator(s) don't have a direct access to the
+file system where the review site is located it gets difficult to
+perform a schema upgrade (run the init program). For such cases it is
+convenient if Gerrit performs schema upgrade automatically on its
+startup.
++
+Since this is a potentially dangerous operation, by default it will not
+be performed. The configuration parameter
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#site.upgradeSchemaOnStartup[
+site.upgradeSchemaOnStartup] is used to switch on automatic schema
+upgrade.
+
+* Shorten column names that are longer than 30 characters
++
+Some databases can't deal with column names that are longer than 30
+characters. Examples are MaxDB and
+link:http://groups.google.com/group/repo-discuss/browse_thread/thread/ecb713d42c04ae8a/cc963525d8247a17?lnk=gst#cc963525d8247a17[Oracle].
++
+Gerrit had two column names in the `accounts` table that exceeded the
+30 characters: `displayPatchSetsInReverseOrder`,
+`displayPersonNameInReviewCategory`
++
+These 2 columns were renamed so that their names fit within the 30
+character range.
+
+* Increase the maximum length for tracking ID's to 32 characters
++
+So far tracking ID's had a maximum length of only 20 characters.
+
+* Set `GERRIT_SITE` in Gerrit hooks as environment variable
++
+Allows development of hooks parametrised on Gerrit location. This can
+be useful to allow hooks to load the Gerrit configuration when needed
+(from `$GERRIT_SITE`) or even store their additional config files under
+`$GERRIT_SITE/etc` and retrieve them at startup.
+
+* Add an exponentially rolling garbage collection script
++
+`git-exproll.sh` is a git garbage collection script aimed specifically
+at reducing exccessive garbage collection and particularly large
+packfile churn for Gerrit installations.
++
+Excessive garbage collection on "dormant" repos is wasteful of both CPU
+and disk IO. Large packfile churn can lead to heavy RAM and FS usage
+on Gerrit servers when the Gerrit process continues to hold open the
+old delete packfiles. This situation is most detrimental when jgit is
+configured with large caching parameters. Aside from these downsides,
+running git gc often can be very beneficial to performance on servers.
+This script attempts to implement a git gc policy which avoids the
+downsides mentioned above so that git gc can be comfortably run very
+regularly.
++
+`git-exproll.sh` uses keep files to manage which files will get
+repacked. It also uses timestamps on the repos to detect dormant repos
+to avoid repacking them at all. The primary packfile objective is to
+keep around a series of packfiles with sizes spaced out exponentially
+from each other, and to roll smaller packfiles into larger ones once
+the smaller ones have grown. This strategy attempts to balance disk
+space usage with avoiding rewriting large packfiles most of the time.
++
+The exponential packing objective above does not save a large amount of
+time or CPU, but it does prevent the packfile churn. Depending on repo
+usage, however the dormant repo detection and avoidance can result in a
+very large time savings.
+
+* Automatically flush persistent H2 cache if the existing cache entries
+ are incompatible with the cache entry class and thus can't be
+ deserialized
+
+* Unpack JARs for running servers in `$site_path/tmp`
++
+Instead of unpacking a running server into `~/.gerritcodereview/tmp`
+only use that location for commands like init where there is no active
+site. From gerrit.sh always use `$site_path/tmp` for the JARs to
+isolate servers that run on the same host under the same UNIX user
+account.
+
+[[custom-extension]]
+* Allow for the `CUSTOM_EXTENSION` `auth.type` to configure URLs for
+ editing the user name and obtaining an HTTP password
++
+Allow `CUSTOM_EXTENSION` auth type to supply by `auth.editFullNameUrl`
+a URL in the web UI that links users to the other account system,
+where they can edit their name, and then use another reload URL to
+cycle through the `/login/` step and refresh the data cached by Gerrit.
++
+Allow `CUSTOM_EXTENSION` auth type to supply by `auth.httpPasswordUrl`
+a URL in the web UI that allows users to obtain an HTTP password.
++
+Like the rest of the `CUSTOM_EXTENSION` stuff, this is hack that will
+eventually go away when there is proper support for authentication
+plugins.
+
+Performance
+~~~~~~~~~~~
+[[performance-issue-on-showing-group-list]]
+* Fix performance issues on showing the list of groups in the Gerrit
+ WebUI
++
+Loading `Admin` > `Groups` on large servers was very slow. The entire
+group membership database was downloaded to the browser when showing
+just the list of groups.
++
+Now the amount of data that needs to be downloaded to the browser is
+reduced by using the more leightweight `AccountGroup` type instead of
+the `GroupDetail` type when showing the groups in a list format. As a
+consequence the `Owners` column that showed the name of the owner group
+had been dropped.
+
+* Add LDAP-cache to minimize number of queries when unnesting groups
++
+A new cache named "ldap_groups_byinclude" is introduced to help lessen
+the number of queries needed to resolve nested LDAP-groups.
+
+* Add index for accessing change messages by patch set
++
+This improves the performance of loading the dashboards.
+
+* Add a fast path to avoid checking every commit on push
++
+If a user can forge author, committer and gerrit server identity, and
+can upload merges, don't bother checking the commit history of what is
+being uploaded. This can save time on servers that are trying to accept
+a large project import using the push permission.
+
+* Improve performance of `ReceiveCommits` by reducing `RevWalk` load
++
+JGit RevWalk does not perform well when a large number of objects are
+added to the start set by `markStart` or `markUninteresting`. Avoid
+putting existing `refs/changes/` or `refs/tags/` into the `RevWalk` and
+instead use only the `refs/heads` namespace and the name of the branch
+used in the `refs/for/` push line.
++
+Catch existing changes by looking for their exact commit SHA-1, rather
+than complete ancestory. This should have roughly the same outcome for
+anyone pushing a new commit on top of an existing open change, but
+with lower computional cost at the server.
+
+* Lookup changes in parallel during `ReceiveCommits`
++
+If the database has high query latency, the loop that locates existing
+changes on the destination branch given Change-Id can be slow. Start
+all of the queries as commits are discovered, but don't block on
+results until all queries were started.
++
+If the database can build the `ResultSet` in the background, this may
+hide some of the query latency by allowing the queries to overlap when
+more than one lookup must be performed for a push.
+
+* Perform change update on multiple threads
++
+When multiple changes need to be created or updated for a single push
+operation they are now inserted into the database by parallel threads,
+up to the maximum allowed thread count. The current thread is used
+when the thread pool is already fully in use, falling back to the
+prior behavior where each concurrent push operation can do its own
+concurrent database update. The thread pool exists to reduce latency
+so long as there are sufficient threads available.
++
+This helps push times on databases that are high latency, such as
+database servers that are running on a different machine from the
+Gerrit server itself, e.g. gerrit.googlesource.com.
++
+The new thread pool is
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#receive.changeUpdateThreads[
+disabled by default], limiting the overhead to servers that have good
+latency with their database, such as using in-process H2 database, or
+a MySQL or PostgreSQL on the same host.
+
+* Use `BatchRefUpdate` to execute reference changes
++
+Some storage backends for JGit are able to update multiple references
+in a single pass efficiently. Take advantage of this by pushing
+any normal reference updates (such as direct push or branch create)
+into a single `BatchRefUpdate` object.
+
+* Assume labels are correct in ListChanges
++
+To reduce end-user latency when displaying changes in a search result
+or user dashboard, assume the labels are accurate in the database at
+display time and don't recompute the access privileges of a reviewer.
+
+* Notify the cache that the git_tags was modified
++
+The tag cache was updated in-place, which prevented the H2 based
+storage from writing out the updated tag information. This meant
+servers almost never had the right data stored on disk and had to
+recompute it at startup.
++
+Anytime the value is now modified in place, put it back into the
+cache so it can be saved for use on the next startup.
+
+* Special case hiding `refs/meta/config` from Git clients
++
+VisibleRefFilter requires a lot of server CPU to accurately provide
+the correct listing to clients when they cannot read `refs/*`.
++
+Since the default configuration is now to link:#hide-config[
+hide `refs/meta/config`], use a special case in VisibleRefFilter that
+permits showing every reference except `refs/meta/config` if a user can
+read every other reference in the repository.
+
+* Avoid second remote call to lookup approvals when loading change
+ results
++
+By using the new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[`/changes/`]
+REST endpoint the web UI client now obtains the label information
+during the query and avoids a second round trip to lookup the current
+approvals for each displayed change. For most users this should improve
+the way the page renders. The verified and code review columns will be
+populated before the table is made visible, preventing the layout from
+"jumping" the way the old UI did when the 2nd RPC finally finished and
+supplied the label data.
+
+* Load patch set approvals in parallel
++
+ResultSet is a future-like interface, the database system is free to
+execute each result set asynchronously in the background if it
+supports that. gwtorm's default SQL backend always runs queries
+immediately and then returns a ListResultSet, so for most installs this
+has no real impact in ordering.
++
+For the system that runs gerrit-review, each query has a high cost in
+network latency, the system treats ResultSet as a future promise to
+supply the matching rows. Getting all of the necessary ResultSets up
+front allows the database to send all requests to the backend as early
+as possible, allowing the network latency to overlap.
+
+Upgrades
+--------
+* Update Gson to 2.1
+* Update GWT to 2.4.0
+* Update JGit to 2.0.0.201206130900-r.23-gb3dbf19
+
+* Use gwtexpui 1.2.6
++
+** Hide superfluous status text from clippy flash widget
+** Fix diappearance of text in CopyableLabel when clicking on it
+
+* Update Guava to 12.0.1
++
+This fixes a performance problem with LoadingCache where the cache's
+inner table did not dynamically resize to handle a larger number
+of cached items, causing O(N) lookup performance for most objects.
+
+Bug Fixes
+---------
+
+Security
+~~~~~~~~
+* Ensure that only administrators can change the global capabilities
++
+Only Gerrit server administrators (members of the groups that have
+the `administrateServer` capability) should be able to edit the
+global capabilities because being able to edit the global capabilities
+means being able to assign the `administrateServer` capability.
++
+Because of this on the `All-Projects` project it is disallowed to assign
++
+. the `owner` access rights on `refs/*`
++
+Project owners (members of groups to which the `owner` access right
+is assigned) are able to edit the access control list of the projects
+they own. Hence being owner of the `All-Projects` project would allow
+to edit the global capabilities and assign the `administrateServer`
+capabilitiy without being Gerrit administrator.
++
+In earlier Gerrit versions (2.1.x) it was already implemented like
+this but the corresponding checks got lost.
++
+. the 'push' access right on `refs/meta/config`
++
+Being able to push configuration changes to the `All-Projects` project
+allows to edit the global capabilities and hence a user with this
+access right could assign the `administrateServer` capability without
+being Gerrit administrator.
++
+From the Gerrit WebUI (ProjectAccessScreen) it is not possible anymore
+to assign on the `All-Projects` project the `owner` access right on
+`refs/*` and the `push` access right on `refs/meta/config`.
++
+In addition it is ensured that an `owner` access right that is assigned
+for `refs/*` on the `All-Projects` project has no effect and that only
+Gerrit administrators with the `push` access right can push
+configuration changes to the `All-Projects` project.
++
+It is still possible to assign both access rights (`owner` on `refs/*`
+and `push` on `refs/meta/config`) on the `All-Projects` project by directly
+editing its `project.config` file and pushing to `refs/meta/config`.
+To fix this it would be needed to reject assigning these access rights
+on the `All-Projects` project as invalid configuration, however doing this
+would mean to break existing configurations of the `All-Projects` project
+that assign these access rights. At the moment there is no migration
+framework in place that would allow to migrate `project.config` files.
+Hence this check is currently not done and these access rights in this
+case have simply no effect.
+
+Web
+~~~
+
+* Do not show "Session cookie not available" on sign in
++
+When LDAP is used for authentication, clicking on the 'Sign In' link
+opens a user/password dialog. In this dialog the "Session cookie not
+available." message was always shown as warning. This warning was
+pretty useless since the user was about to sign in because he had no
+current session.
++
+This problem was discussed on the
+link:https://groups.google.com/forum/#!topic/repo-discuss/j-t77m8-7I0/discussion[
+Gerrit mailing list].
+
+* Reject restoring a change if its destination branch does not exist
+ anymore
+
+* Reject submitting a change if its destination branch does not exist
+ anymore
++
+If a branch got deleted and there was an open change for this branch,
+it was still possible to submit this open change. As result the
+destination branch was implicitly recreated, even if the user
+submitting the change had no privileges to create branches.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1352[issue 1352]:
+ Don't display "Download" link for `/COMMIT_MSG`
++
+The commit message file is special, it doesn't actually exist and
+cannot be downloaded. Don't offer the download link in the side by
+side viewer.
+
+* Dependencies were lost in the ChangeScreen's "Needed By" table
++
+Older patchsets are now iterated for decendents, so that the dependency
+chain does not break on new upstream patchsets.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1442[issue 1442]:
+ Only show draft change dependency if current user is owner or reviewer
++
+In the change screen, the dependencies panel was showing draft changes
+in the "Depends On" and "Needed By" lists for all users, and when there
+was no user logged in.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1558[issue 1558]:
+ Create a draft patch set when a draft patch set is rebased
++
+Rebasing a draft patch set created a non-draft patch set. It was
+unexpected that rebasing a draft patch set published the modifications
+done in the draft patch set.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1176[issue 1176]:
+ Fix disappearance of download command in Firefox
++
+Clicking on the download command for a patch set in Firefox made the
+download command disappear.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1587[issue 1587]:
+ Fix disappearance of action buttons when selecting the last patch set
+ as `Old Version History`
+
+* Fix updating patch list when `Old Version History` is changed
++
+If a collapsed patch set panel was expanded and re-closed it's patch
+list wasn't updated anymore when the selection for `Old Version History`
+was changed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1523[issue 1523]:
+ Update diff base to match old version history
++
+When changing the diff base in the `Old Version History` on the change
+screen and then entering the Side-By-Side view for a file, clicking on
+the back button in the browser (reentering the change screen) was
+causing the files to be wrongly compared with `Base` again.
+
+* Don't NPE if current patch set is not available
++
+Broken changes may have the current patch set field incorrectly
+specified, causing currentPatchSet to be unable to locate the
+correct data and return it. When this happens don't NPE, just
+claim the change is not reviewed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1555[issue 1555]:
+ Fix displaying of file diff if draft patch has been deleted
++
+Displaying any file diff for a patch set failed if the change had any
+gaps in its patch set history. Patch sets can be missing, if they
+have been drafts and were deleted.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=856[issue 856]:
+ Fix displaying of comments on deleted files
++
+Published and draft comments that are posted on deleted files were not
+loaded and displayed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=735[issue 735]:
+ Fix `ArrayIndexOutOfBoundsException` on navigation to next/previous
+ patch
++
+An `ArrayIndexOutOfBoundsException` could occur when navigating from
+one patch to the next/previous patch if the next/previous patch was a
+newly added binary file. The exception occurred if the user was not
+signed in or if the user was signed in and had `Syntax Coloring` in the
+preferences enabled.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=816[issue 816]:
+ Fix wrong file indention in Side-by-Sie diff viewer on right side
+
+* Only set reviewed attribute on open changes
++
+If a change is merged or abandoned, do not consider the reviewed
+property for the calling user, so that the change is not highlighted
+as unreviewed on the user's dashboard.
+
+* Change PatchTable pointer when loading patch
++
+This patch fixes an issue with the "file list" table displayed by
+clicking on the "Files" sub-menu when viewing a diff.
++
+Originally when navigating between patch screens the highlighted row
+(pointer) of the file list table would not change when not directly
+interacting with the table e.g. by clicking on the previous or next
+file link.
++
+This patch updates the file list table whenever a new patch screen is loaded
+so that the pointer corresponds to the current patch being displayed.
+
+* Don't hyperlink non-internal groups
++
+When an external group (such as LDAP) is used in a permission rule,
+don't attempt to link to the group in the internal account system UI.
+The group won't load successfully. Instead just display the name and
+put the UUID into a tooltip to show the full DN.
+
+* Fix: Popup jumps back to original position when resizing screen
++
+On 'Watched Projects' screen, the 'Browse' button displays a popup
+window. If the user moves it and then resizes the screen, it won't snap
+back to the original position.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1457[issue 1457]:
+ Prevent groups from being renamed to empty string
+
+* Fixed AccountGroupInfoScreen search callback
++
+If the search returned no results, the search button would not be
+enabled and the status panel was not shown. Fixed the panel and button
+to always be enabled.
+
+* Fix NullPointerException on `/p/`
++
+Requesting just `/p/` caused a NullPointerException as the redirection
+logic had no project name to form a URL from. Detect requests for `/p/`
+and redirect to 'Admin' > 'Projects' to show the projects the caller
+has access to.
+
+Mail
+~~~~
+
+* Fix: Rebase did not mail all reviewers
+
+* Fix email showing in AccountLink instead of names
++
+Prefer the full name for the display text of the link.
+
+* Fix signature delimiter for e-mail messages
++
+Make sure the signature delimiter is "-- " (two dashes and a space).
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1397[issue 1397]:
+ Don't wait for banner message from SMTP server after STARTTLS
+ negotiation
++
+According to RFC 2847 section 5.2, SMTP server won't send the banner
+message again after STARTTLS negotiation. The original code will hang
+until SMTP server kicks it off due to timeout and can't send email with
+STARTTLS enabled, aka. `sendemail.smtpEncryption = tls`.
+
+* Extract all mail templates during site init
++
+The example mail templates `RebasedPatchSet.vm`, `Restored.vm` and
+`Reverted.vm` were not extracted during the initialization of a new
+site.
+
+SSH
+~~~
+* Fix reject message if bypassing code review is not allowed
++
+If a user is not allowed to bypass code review, but tries to push a
+commit directly, Gerrit rejected this push with the error message
+"can not update the reference as a fast forward". This message was
+confusing to the user since the push only failed due to missing
+access rights. Go back to the old message that says "prohibited
+by Gerrit".
+
+* Fix reject message if pushing tag is rejected because tagger is
+ somebody else
++
+Pushing a tag that has somebody else as tagger requires the `Forge
+Committer` access right. If this access right was missing Gerrit
+was rejecting the push with "can not create new references". This error
+message was misleading because the user may have thought that the
+`Create Reference` access right was missing which was actually assigned.
++
+The same reject message was also returned on push of an annotated tag
+if the `Push Annotated Tag` access right was missing. Also in this case
+the error message was not ideal.
++
+Go back to the old more generic message which says `prohibited by
+Gerrit`.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1437[issue 1437]:
+ Send event to stream when draft change is published
++
+When a change is uploaded as a draft, a `patchset-created` event is
+sent to the event stream, but since drafts are private to the owner,
+the event is not publicly visible. When the draft is later published,
+no publicly visible event was sent. As result of this external tools
+that rely on the event stream to detect new changes didn't receive
+events for any changes that were first uploaded as draft.
++
+There is now a new event, `draft-published`, which is sent to the
+event stream when a draft change is published. The content of this
+event is the same as `patchset-created`.
+
+* Fix: Wrong ps/rev in `change-merged` stream-event
++
+When using cherry-pick as merge strategy, the wrong ref was set in the
+`change-merged` stream-event.
++
+The issue stems from Gerrit would not acknowledge the resulting new
+pachset (the actual cherry-pick).
+
+* Fix NullPointerException in `query` SSH command
++
+Running the `query` SSH command with the options `--comments` and
+`--format=JSON` failed with a NullPointerException if a change had a
+message without author. Change messages have no author if they were
+created by Gerrit. For such messages now the Gerrit Server identity is
+returned as author.
+
+* Fix the `export-review-notes` command's Guice bindings
++
+The `export-review-notes` command was broken becasue of the CachePool
+class being bound twice. The startup of the command failed because of
+that.
+
+* Fix sorting of SSH help text
++
+Commands were displaying in random order, sort commands before output.
+
+* `replicate` command: Do not log errors for wrong user input
++
+If the user provided an invalid combination of command options or an
+non existing project name this was logged in the `error.log` but
+printing the error out to the user is sufficient.
+
+Authentication
+~~~~~~~~~~~~~~
+
+* Fix NPE in LdapRealm caused by non-LDAP users
++
+Servers that are connected to LDAP but have non-LDAP user accounts
+created by `gerrit create-account` (e.g. batch role accounts for
+build systems) were crashing with a NullPointerException when the
+LdapRealm tried to discover which LDAP groups the non-LDAP user
+was a member of in the directory.
+
+* Fix domain field of HTTP digest authentication
++
+Per RFC 2617 the domain field is optional. If it is not present,
+the digest token is valid on any URL on the server. When set it
+must be a path prefix describing the URLs that the password would
+be valid against.
++
+When a canonical URL is known, supply that as the only domain that
+is valid. When the URL is missing (e.g. because the provider is
+still broken) rely on the context path of the application instead.
+
+Replication
+~~~~~~~~~~~
+
+* Fix inconsistent behaviour when replicating `refs/meta/config`
++
+In `replication.config`, if `authGroup` is set to be used together with
+`mirror = true`, refs blocked through the `authGroup` are deleted from
+the slave/mirror. The same correctly applies if the `authGroup` is used
+to block `refs/meta/config`.
++
+However, if `replicatePermission` was set to `false`, Gerrit was
+refusing to clean up `refs/meta/config` on the slave/mirror.
+
+* Fix bug with member assignment order in PushReplication.
++
+The groupCache was being used before it was set in the class. Fix the
+ordering of the assignment.
+
+Approval Categories
+~~~~~~~~~~~~~~~~~~~
+
+* Make `NoBlock` and `NoOp` approval category functions work
++
+The approval category functions `NoBlock` and `NoOp` have not worked
+since the integration of Prolog.
++
+`MAY` was introduced as a new submit record status to complement `OK`,
+`REJECT`, `NEED`, and `IMPOSSIBLE`. This allows the expression of
+approval categories (labels) that are optional, i.e. could either be
+set or unset without ever influencing whether the change could be
+submitted. Previously there was no way to express this property in
+the submit record.
++
+This enables the `NoBlock` and `NoOp` approval category functions to
+work as they now emit may() terms from the Prolog rules. Previously
+they returned ok() terms lacking a nested user term, leading to
+exceptions in code that expected a user context if the label was `OK`.
+
+* Fix category block status without negative score
++
+Categories without blocking or approval scores will result in the
+blocking/approved image appearing in the category column after changes
+are merged should the score by the reviewer match the minimum or
+maximum value respectively.
++
+A check to ignore "No Score" values of 0 was added.
+
+* Don't remove dashes from approval category name
++
+If an approval category name contained a dash, it was removed by
+Gerrit. On the other side a space in an approval category name is
+converted to a dash. This was confusing for writing Prolog submit
+rules. If, for example, one defined a new category named `X-Y`, then in
+the Prolog code the proper name for that category would have been `XY`
+which was unintuitive.
+
+* Fix NPE in `PRED__load_commit_labels_1`
++
+If a change query uses reviewer information and loads the approvals
+map, but there are no approvals for a given patch set available, the
+collection came out null, which cannot be iterated. Make it always be
+an empty list.
+
+Other
+~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1554[issue 1554]:
+ Fix cloning of new projects from slave servers
++
+If a new project is created in Gerrit the replication creates the
+repository for this new project directly in the filesystem of the slave
+server. The slave server was not discovering this new repository and as
+result any attempt to clone the corresponding project from the slave
+server failed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1548[issue 1548]:
+ Create a ref for the patch set that is created when a change is
+ cherry-picked and trigger the replication for it:
++
+If Cherry Pick is chosen as submit type, on submit a new commit is
+created by the cherry-pick. For this commit a new patch set is created
+which is added to the change. Using any of the download commands to
+fetch this new patch set failed with 'Couldn't find remote ref' because
+no ref for the new patch set was created.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1626[issue 1626]:
+ Fix NullPointerException on cherry-pick if `changeMerge.test` is enabled
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1491[issue 1491]:
+ Fix nested submodule updates
+
+* Set link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#transfer.timeout[transfer
+ timeout] for pushes through HTTP
++
+The transfer timeout was only set when pushing via SSH.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#receive.maxObjectSizeLimit[
+ Limit maximum Git object size] when pushing through HTTP
++
+The limit for the maximum object size was only set when pushing via SSH.
+
+* Fix units of `httpd.maxwait`
++
+The default unit here is minutes, but Jetty wants to get milliseconds
+from the maxWait field. Convert the minutes returned by getTimeUnit to
+be milliseconds, matching what Jetty expects.
++
+This should resolve a large number of 503 errors for Git over HTTP.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1493[issue 1493]:
+ Fix wrong "change ... closed" message on direct push
++
+Pushing a commit directly into the central repository with bypassing
+code review wrongly resulted in a "change ... closed" message if the
+commit was already pushed for review and if a Change-Id was included in
+the commit message. Despite of the error message the push succeeded and
+the corresponding change got closed. Now the message is not printed
+anymore.
+
+* Fix NPE that can hide guice CreationException on site init
++
+Note that the `--show-stack-trace` option is needed to print the stack
+trace when a program stops with a Die exception.
+
+* Do not automatically add author/committer as reviewer to drafts
+
+* Do not automatically add reviewers from footer lines to drafts
+
+* Fix NullPointerException in MergeOp
++
+The body of the commit object may have been discarded earlier to
+save memory, so ensure it exists before asking for the author.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1396[issue 1396]:
+ Initialize the submodule commit message buffer
+
+* Fix file name matching in `commit_delta` to perform substring
+ matching
++
+The `commit_delta` predicate was matching the entire file name against
+the given regular expression while other predicates (`commit_edits`,
+`commit_message_matches`) performed substring matching. It was
+inconsistent that for `commit_delta` it was needed to write something
+like:
++
+----
+ commit_delta('.*\.java')
+----
++
+to match all `*.java` files, while for `commit_edits` it was:
++
+----
+ commit_edits('\.java$', '...')
+----
++
+to match the same set of (Java) files.
+
+* Create index for submodule subscriptions on site upgrade
+
+* Fix URL to Jetty XML DTDs so they can be properly validated
+
+* Fix resource leak when `changeMerge.test` is `true`
+
+* Fix possible synchronization issue in TaskThunk
+
+* Fix possible NPEs in `ReplaceRequest.cmd` usage in `ReceiveCommits`
++
+The `cmd` field is populated by `validate(boolean)`. If this method
+fails, results on some `ReplaceRequests` may not be set. Guard the
+attempt to access the field with a null check.
+
+* Match no labels if current patch set is not available
++
+If the current patch set cannot be loaded from `ChangeData`, assume no
+label information. This works around an NullPointerException inside of
+`ChangeControl` where the `PatchSet` is otherwise required.
+
+* Create new patch set references before database records
++
+Ensure the commit used by a new change or replacement patch set
+always exists in the Git repository by writing the reference first
+as part of the overall `BatchRefUpdate`, then inserting the database
+records if all of the references stored successfully.
+
+* Fix rebase patch set and revert change to update Git first
++
+Update the Git reference before writing to the database. This way the
+repository cannot be corrupted if the server goes down between the two
+actions.
+
+* Make sure we use only one type of NoteMerger for review notes creation
+
+* Fix generation of owner group in GroupDetail
++
+Set the GroupDetail.ownerGroup to the AccountGroup.ownerGroupUUID
+instead of the groupUUID.
+
+* Ensure that ObjectOutputStream in H2CacheImpl is closed
+
+* Ensure that RevWalk in SubmoduleOp is released
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 5f8de28d0d..07e6aa5203 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,15 @@
Gerrit Code Review - Release Notes
==================================
+[[2_5]]
+Version 2.5.x
+-------------
+* link:ReleaseNotes-2.5.4.html[2.5.4]
+* link:ReleaseNotes-2.5.3.html[2.5.3]
+* link:ReleaseNotes-2.5.2.html[2.5.2]
+* link:ReleaseNotes-2.5.1.html[2.5.1]
+* link:ReleaseNotes-2.5.html[2.5]
+
[[2_4]]
Version 2.4.x
-------------
@@ -11,26 +20,26 @@ Version 2.4.x
[[2_3]]
Version 2.3.x
-------------
-* link:ReleaseNotes-2.3.html[2.3]
* link:ReleaseNotes-2.3.1.html[2.3.1]
+* link:ReleaseNotes-2.3.html[2.3]
[[2_2]]
Version 2.2.x
-------------
-* link:ReleaseNotes-2.2.2.html[2.2.2],
-* link:ReleaseNotes-2.2.2.2.html[2.2.2.2],
-* link:ReleaseNotes-2.2.2.1.html[2.2.2.1],
-* link:ReleaseNotes-2.2.1.html[2.2.1],
+* link:ReleaseNotes-2.2.2.2.html[2.2.2.2]
+* link:ReleaseNotes-2.2.2.1.html[2.2.2.1]
+* link:ReleaseNotes-2.2.2.html[2.2.2]
+* link:ReleaseNotes-2.2.1.html[2.2.1]
* link:ReleaseNotes-2.2.0.html[2.2.0]
[[2_1]]
Version 2.1.x
-------------
-* link:ReleaseNotes-2.1.8.html[2.1.8],
-* link:ReleaseNotes-2.1.7.2.html[2.1.7.2],
-* link:ReleaseNotes-2.1.7.html[2.1.7],
-* link:ReleaseNotes-2.1.6.html[2.1.6],
- link:ReleaseNotes-2.1.6.1.html[2.1.6.1]
+* link:ReleaseNotes-2.1.8.html[2.1.8]
+* link:ReleaseNotes-2.1.7.2.html[2.1.7.2]
+* link:ReleaseNotes-2.1.7.html[2.1.7]
+* link:ReleaseNotes-2.1.6.1.html[2.1.6.1]
+* link:ReleaseNotes-2.1.6.html[2.1.6]
* link:ReleaseNotes-2.1.5.html[2.1.5]
* link:ReleaseNotes-2.1.4.html[2.1.4]
* link:ReleaseNotes-2.1.3.html[2.1.3]
@@ -40,31 +49,31 @@ Version 2.1.x
* link:ReleaseNotes-2.1.2.2.html[2.1.2.2]
* link:ReleaseNotes-2.1.2.1.html[2.1.2.1]
* link:ReleaseNotes-2.1.2.html[2.1.2]
-* link:ReleaseNotes-2.1.1.html[2.1.1],
- link:ReleaseNotes-2.1.1.html[2.1.1.1]
+* link:ReleaseNotes-2.1.1.html[2.1.1.1]
+* link:ReleaseNotes-2.1.1.html[2.1.1]
* link:ReleaseNotes-2.1.html[2.1]
[[2_0]]
Version 2.0.x
-------------
-* link:ReleaseNotes-2.0.24.html[2.0.24],
- link:ReleaseNotes-2.0.24.html[2.0.24.1],
- link:ReleaseNotes-2.0.24.html[2.0.24.2]
+* link:ReleaseNotes-2.0.24.html[2.0.24.2]
+* link:ReleaseNotes-2.0.24.html[2.0.24.1]
+* link:ReleaseNotes-2.0.24.html[2.0.24]
* link:ReleaseNotes-2.0.23.html[2.0.23]
* link:ReleaseNotes-2.0.22.html[2.0.22]
* link:ReleaseNotes-2.0.21.html[2.0.21]
* link:ReleaseNotes-2.0.20.html[2.0.20]
-* link:ReleaseNotes-2.0.19.html[2.0.19],
- link:ReleaseNotes-2.0.19.html[2.0.19.1],
- link:ReleaseNotes-2.0.19.html[2.0.19.2]
+* link:ReleaseNotes-2.0.19.html[2.0.19.2]
+* link:ReleaseNotes-2.0.19.html[2.0.19.1]
+* link:ReleaseNotes-2.0.19.html[2.0.19]
* link:ReleaseNotes-2.0.18.html[2.0.18]
* link:ReleaseNotes-2.0.17.html[2.0.17]
* link:ReleaseNotes-2.0.16.html[2.0.16]
* link:ReleaseNotes-2.0.15.html[2.0.15]
-* link:ReleaseNotes-2.0.14.html[2.0.14],
- link:ReleaseNotes-2.0.14.html[2.0.14.1]
-* link:ReleaseNotes-2.0.13.html[2.0.13],
- link:ReleaseNotes-2.0.13.html[2.0.13.1]
+* link:ReleaseNotes-2.0.14.html[2.0.14.1]
+* link:ReleaseNotes-2.0.14.html[2.0.14]
+* link:ReleaseNotes-2.0.13.html[2.0.13.1]
+* link:ReleaseNotes-2.0.13.html[2.0.13]
* link:ReleaseNotes-2.0.12.html[2.0.12]
* link:ReleaseNotes-2.0.11.html[2.0.11]
* link:ReleaseNotes-2.0.10.html[2.0.10]
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index e766ef1de4..553ab34092 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -5,7 +5,7 @@ Short Version:
- Make sure all code is under the Apache License, 2.0.
- Publish your changes for review:
- git push https://gerrit-review.googlesource.com/gerrit HEAD:refs/for/master
+ git push https://gerrit.googlesource.com/gerrit HEAD:refs/for/master
Long Version:
@@ -70,7 +70,7 @@ Ensure you have obtained a unique HTTP password to identify yourself:
Push your patches over HTTPS to the review server, possibly through
a remembered remote to make this easier in the future:
- git config remote.review.url https://google-review.googlesource.com/gerrit
+ git config remote.review.url https://gerrit.googlesource.com/gerrit
git config remote.review.push HEAD:refs/for/master
git push review
diff --git a/contrib/git-exproll.sh b/contrib/git-exproll.sh
new file mode 100644
index 0000000000..9526d9ff78
--- /dev/null
+++ b/contrib/git-exproll.sh
@@ -0,0 +1,566 @@
+#!/bin/bash
+# Copyright (c) 2012, Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+# # Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# # Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# # Neither the name of Code Aurora Forum, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+usage() { # error_message
+
+ cat <<-EOF
+ usage: $(basename $0) [-unvt] [--noref] [--nolosse] [-r|--ratio number]
+ [git gc option...] git.repo
+
+ -u|-h usage/help
+ -v verbose
+ -n dry-run don't actually repack anything
+ -t touch treat repo as if it had been touched
+ --noref avoid extra ref packing timestamp checking
+ --noloose do not run just because there are loose object dirs
+ (repacking may still run if they are referenced)
+ -r ratio <number> packfile ratio to aim for (default 10)
+
+ git gc option will be passed as args to git gc
+
+ git.repo to run gc against
+
+ Garbage collect using a pseudo logarithmic packfile maintenance
+ approach. This approach attempts to minimize packfile churn
+ by keeping several generations of varying sized packfiles around
+ and only consolidating packfiles (or loose objects) which are
+ either new packfiles, or packfiles close to the same size as
+ another packfile.
+
+ An estimate is used to predict when rollups (one consolidation
+ would cause another consolidation) would occur so that this
+ rollup can be done all at once via a single repack. This reduces
+ both the runtime and the pack file churn in rollup cases.
+
+ Approach: plan each consolidation by creating a table like this:
+
+ Id Keep Size Sha1(or consolidation list) Actions(repack down up note)
+ 1 - 11356 9052edfb7392646cd4e5f362b953675985f01f96 y - - New
+ 2 - 429088 010904d5c11cd26a79fda91b01ab454d1001b402 y - - New
+ c1 - 440444 [1,2] - - -
+
+ Id: numbers preceded by a c are estimated "c pack" files
+ Keep: - none, k private keep, o our keep
+ Size: in disk blocks (default du output)
+ Sha1: of packfile, or consolidation list of packfile ids
+ Actions
+ repack: - n no, y yes
+ down: - noop, ^ consolidate with a file above
+ up: - noop, v consolidate with a file below
+ note: Human description of script decisions:
+ New (file is a new packfile)
+ Consolidate with:<list of packfile ids>
+ (too far from:<list of packfile ids>)
+
+ On the first pass, always consolidate any new packfiles along
+ with loose objects and along with any packfiles which are within
+ the ratio size of their predecessors (note, the list is ordered
+ by increasing size). After each consolidation, insert a fake
+ consolidation, or "c pack", to naively represent the size and
+ ordered positioning of the anticipated new consolidated pack.
+ Every time a new pack is planned, rescan the list in case the
+ new "c pack" would cause more consolidation...
+
+ Once the packfiles which need consolidation are determined, the
+ packfiles which will not be consolidated are marked with a .keep
+ file, and those which will be consolidated will have their .keep
+ removed if they have one. Thus, the packfiles with a .keep will
+ not get repacked.
+
+ Packfile consolidation is determined by the --ratio parameter
+ (default is 10). This ratio is somewhat of a tradeoff. The
+ smaller the number, the more packfiles will be kept on average;
+ this increases disk utilization somewhat. However, a larger
+ ratio causes greater churn and may increase disk utilization due
+ to deleted packfiles not being reclaimed since they may still be
+ kept open by long running applications such as Gerrit. Sane
+ ratio values are probably between 2 and 10. Since most
+ consolidations actually end up smaller than the estimated
+ consolidated packfile size (due to compression), the true ratio
+ achieved will likely be 1 to 2 greater than the target ratio.
+ The smaller the target ratio, the greater this discrepancy.
+
+ Finally, attempt to skip garbage collection entirely on untouched
+ repos. In order to determine if a repo has been touched, use the
+ timestamp on the script's keep files, if any relevant file/dir
+ is newer than a keep marker file, assume that the repo has been
+ touched and gc needs to run. Also assume gc needs to run whenever
+ there are loose object dirs since they may contain untouched
+ unreferenced loose objects which need to be pruned (once they
+ expire).
+
+ In order to allow the keep files to be an effective timestamp
+ marker to detect relevant changes in a repo since the last run,
+ all relevant files and directories which may be modified during a
+ gc run (even during a noop gc run), must have their timestamps
+ reset to the same time as the keep files or gc will always run
+ even on untouched repos. The relevant files/dirs are all those
+ files and directories which garbage collection, object packing,
+ ref packing and pruning might change during noop actions.
+EOF
+
+ [ -n "$1" ] && info "ERROR $1"
+
+ exit
+}
+
+debug() { [ -n "$SW_V" ] && info "$1" ; }
+info() { echo "$1" >&2 ; }
+
+array_copy() { #v2 # array_src array_dst
+ local src=$1 dst=$2
+ local s i=0
+ eval s=\${#$src[@]}
+ while [ $i -lt $s ] ; do
+ eval $dst[$i]=\"\${$src[$i]}\"
+ i=$(($i + 1))
+ done
+}
+
+array_equals() { #v2 # array_name [vals...]
+ local a=$1 ; shift
+ local s=0 t=() val
+ array_copy "$a" t
+ for s in "${!t[@]}" ; do s=$((s+1)) ; done
+ [ "$s" -ne "$#" ] && return 1
+ for val in "${t[@]}" ; do
+ [ "$val" = "$1" ] || return 2
+ shift
+ done
+ return 0
+}
+
+packs_sizes() { # git.repo > "size pack"...
+ du -s "$1"/objects/pack/pack-$SHA1.pack | sort -n 2> /dev/null
+}
+
+is_ourkeep() { grep -q "$KEEP" "$1" 2> /dev/null ; } # keep
+has_ourkeep() { is_ourkeep "$(keep_for "$1")" ; } # pack
+has_keep() { [ -f "$(keep_for "$1")" ] ; } # pack
+is_repo() { [ -d "$1/objects" ] && [ -d "$1/refs/heads" ] ; } # git.repo
+
+keep() { # pack # returns true if we added our keep
+ keep=$(keep_for "$1")
+ [ -f "$keep" ] && return 1
+ echo "$KEEP" > "$keep"
+ return 0
+}
+
+keep_for() { # packfile > keepfile
+ local keep=$(echo "$1" | sed -es'/\.pack$/.keep/')
+ [ "${keep/.keep}" = "$keep" ] && return 1
+ echo "$keep"
+}
+
+idx_for() { # packfile > idxfile
+ local idx=$(echo "$1" | sed -es'/\.pack$/.idx/')
+ [ "${idx/.idx}" = "$idx" ] && return 1
+ echo "$idx"
+}
+
+# pack_or_keep_file > sha
+sha_for() { echo "$1" | sed -es'|\(.*/\)*pack-\([^.]*\)\..*$|\2|' ; }
+
+private_keeps() { # git.repo -> sets pkeeps
+ local repo=$1 ary=$2
+ local keep keeps=("$repo"/objects/pack/pack-$SHA1.keep)
+ pkeeps=()
+ for keep in "${keeps[@]}" ; do
+ is_ourkeep "$keep" || pkeeps=("${pkeeps[@]}" "$keep")
+ done
+}
+
+is_tooclose() { [ "$(($1 * $RATIO))" -gt "$2" ] ; } # smaller larger
+
+unique() { # [args...] > unique_words
+ local lines=$(while [ $# -gt 0 ] ; do echo "$1" ; shift ; done)
+ lines=$(echo "$lines" | sort -u)
+ echo $lines # as words
+}
+
+outfs() { # fs [args...] > argfs...
+ local fs=$1 ; shift
+ [ $# -gt 0 ] && echo -n "$1" ; shift
+ while [ $# -gt 0 ] ; do echo -n "$fs$1" ; shift ; done
+}
+
+sort_list() { # < list > formatted_list
+ # n has_keep size sha repack down up note
+ awk '{ note=$8; for(i=8;i<NF;i++) note=note " "$(i+1)
+ printf("%-5s %s %-14s %-40s %s %s %s %s\n", \
+ $1,$2, $3, $4, $5,$6,$7,note)}' |\
+ sort -k 3,3n -k 1,1n
+}
+
+is_touched() { # git.repo
+ local repo=$1
+ local loose keep ours newer
+ [ -n "$SW_T" ] && { debug "$SW_T -> treat as touched" ; return 0 ; }
+
+ if [ -z "$SW_LOOSE" ] ; then
+ # If there are loose objects, they may need to be pruned,
+ # run even if nothing has really been touched.
+ loose=$(find "$repo/objects" -type d \
+ -wholename "$repo/objects/[0-9][0-9]"
+ -print -quit 2>/dev/null)
+ [ -n "$loose" ] && { info "There are loose object directories" ; return 0 ; }
+ fi
+
+ # If we don't have a keep, the current packfiles may not have been
+ # compressed with the current gc policy (gc may never have been run),
+ # so run at least once to repack everything. Also, we need a marker
+ # file for timestamp tracking (a dir needs to detect changes within
+ # it, so it cannot be a marker) and our keeps are something we control,
+ # use them.
+ for keep in "$repo"/objects/pack/pack-$SHA1.keep ; do
+ is_ourkeep "$keep" && { ours=$keep ; break ; }
+ done
+ [ -z "$ours" ] && { info 'We have no keep (we have never run?): run' ; return 0 ; }
+
+ debug "Our timestamp keep: $ours"
+ # The wholename stuff seems to get touched by a noop git gc
+ newer=$(find "$repo/objects" "$repo/refs" "$repo/packed-refs" \
+ '!' -wholename "$repo/objects/info" \
+ '!' -wholename "$repo/objects/info/*" \
+ -newer "$ours" \
+ -print -quit 2>/dev/null)
+ [ -z "$newer" ] && return 1
+
+ info "Touched since last run: $newer"
+ return 0
+}
+
+touch_refs() { # git.repo start_date refs
+ local repo=$1 start_date=$2 refs=$3
+ (
+ debug "Setting start date($start_date) on unpacked refs:"
+ debug "$refs"
+ cd "$repo/refs" || return
+ # safe to assume no newlines in a ref name
+ echo "$refs" | xargs -d '\n' -n 1 touch -c -d "$start_date"
+ )
+}
+
+set_start_date() { # git.repo start_date refs refdirs packedrefs [packs]
+ local repo=$1 start_date=$2 refs=$3 refdirs=$4 packedrefs=$5 ; shift 5
+ local pack keep idx repacked
+
+ # This stuff is touched during object packs
+ while [ $# -gt 0 ] ; do
+ pack=$1 ; shift
+ keep="$(keep_for "$pack")"
+ idx="$(idx_for "$pack")"
+ touch -c -d "$start_date" "$pack" "$keep" "$idx"
+ debug "Setting start date on: $pack $keep $idx"
+ done
+ # This will prevent us from detecting any deletes in the pack dir
+ # since gc ran, except for private keeps which we are checking
+ # manually. But there really shouldn't be any other relevant deletes
+ # in this dir which should cause us to rerun next time, deleting a
+ # pack or index file by anything but gc would be bad!
+ debug "Setting start date on pack dir: $start_date"
+ touch -c -d "$start_date" "$repo/objects/pack"
+
+
+ if [ -z "$SW_REFS" ] ; then
+ repacked=$(find "$repo/packed-refs" -newer "$repo/objects/pack"
+ -print -quit 2>/dev/null)
+ if [ -n "$repacked" ] ; then
+ # The ref dirs and packed-ref files seem to get touched even on
+ # a noop refpacking
+ debug "Setting start date on packed-refs"
+ touch -c -d "$start_date" "$repo/packed-refs"
+ touch_refs "$repo" "$start_date" "$refdirs"
+
+ # A ref repack does not imply a ref change, but since it is
+ # hard to tell, simply assume so
+ if [ "$refs" != "$(cd "$repo/refs" ; find -depth)" ] || \
+ [ "$packedrefs" != "$(<"$repo/packed-refs")" ] ; then
+ # We retouch if needed (instead of simply checking then
+ # touching) to avoid a race between the check and the set.
+ debug " but refs actually got packed, so retouch packed-refs"
+ touch -c "$repo/packed-refs"
+ fi
+ fi
+ fi
+}
+
+note_consolidate() { # note entry > note (no duplicated consolidated entries)
+ local note=$1 entry=$2
+ local entries=() ifs=$IFS
+ if echo "$note" | grep -q 'Consolidate with:[0-9,c]' ; then
+ IFS=,
+ entries=( $(echo "$note" | sed -es'/^.*Consolidate with:\([0-9,c]*\).*$/\1/') )
+ note=( $(echo "$note" | sed -es'/Consolidate with:[0-9,c]*//') )
+ IFS=$ifs
+ fi
+ entries=( $(unique "${entries[@]}" "$entry") )
+ echo "$note Consolidate with:$(outfs , "${entries[@]}")"
+}
+
+note_toofar() { # note entry > note (no duplicated "too far" entries)
+ local note=$1 entry=$2
+ local entries=() ifs=$IFS
+ if echo "$note" | grep -q '(too far from:[0-9,c]*)' ; then
+ IFS=,
+ entries=( $(echo "$note" | sed -es'/^.*(too far from:\([0-9,c]*\)).*$/\1/') )
+ note=( $(echo "$note" | sed -es'/(too far from:[0-9,c]*)//') )
+ IFS=$ifs
+ fi
+ entries=( $(unique "${entries[@]}" "$entry") )
+ echo "$note (too far from:$(outfs , "${entries[@]}"))"
+}
+
+last_entry() { # isRepack pline repackline > last_rows_entry
+ local size_hit=$1 pline=$2 repackline=$3
+ if [ -n "$pline" ] ; then
+ if [ -n "$size_hit" ] ; then
+ echo "$repack_line"
+ else
+ echo "$pline"
+ fi
+ fi
+}
+
+init_list() { # git.repo > shortlist
+ local repo=$1
+ local file
+ local n has_keep size sha repack
+
+ packs_sizes "$1" | {
+ while read size file ; do
+ n=$((n+1))
+ repack=n
+ has_keep=-
+ if has_keep "$file" ; then
+ has_keep=k
+ has_ourkeep "$file" && has_keep=o
+ fi
+ sha=$(sha_for "$file")
+ echo "$n $has_keep $size $sha $repack"
+ done
+ } | sort_list
+}
+
+consolidate_list() { # run < list > list
+ local run=$1
+ local sum=0 psize=0 sum_size=0 size_hit pn clist pline repackline
+ local n has_keep size sha repack down up note
+
+ {
+ while read n has_keep size sha repack down up note; do
+ [ -z "$up" ] && up='-'
+ [ -z "$down" ] && down="-"
+
+ if [ "$has_keep" = "k" ] ; then
+ echo "$n $has_keep $size $sha $repack - - Private"
+ continue
+ fi
+
+ if [ "$repack" = "n" ] ; then
+ if is_tooclose $psize $size ; then
+ size_hit=y
+ repack=y
+ sum=$(($sum + $sum_size + $size))
+ sum_size=0 # Prevents double summing this entry
+ clist=($(unique "${clist[@]}" $pn $n))
+ down="^"
+ [ "$has_keep" = "-" ] && note="$note New +"
+ note=$(note_consolidate "$note" "$pn")
+ elif [ "$has_keep" = "-" ] ; then
+ repack=y
+ sum=$(($sum + $size))
+ sum_size=0 # Prevents double summing this entry
+ clist=($(unique "${clist[@]}" $n))
+ note="$note New"
+ elif [ $psize -ne 0 ] ; then
+ sum_size=$size
+ down="!"
+ note=$(note_toofar "$note" "$pn")
+ else
+ sum_size=$size
+ fi
+ else
+ sum_size=$size
+ fi
+
+ # By preventing "c files" (consolidated) from being marked
+ # "repack" they won't get keeps
+ repack2=y
+ [ "${n/c}" != "$n" ] && { repack=- ; repack2=- ; }
+
+ last_entry "$size_hit" "$pline" "$repack_line"
+ # Delay the printout until we know whether we are
+ # being consolidated with the entry following us
+ # (we won't know until the next iteration).
+ # size_hit is used to determine which of the lines
+ # below will actually get printed above on the next
+ # iteration.
+ pline="$n $has_keep $size $sha $repack $down $up $note"
+ repack_line="$n $has_keep $size $sha $repack2 $down v $note"
+
+ pn=$n ; psize=$size # previous entry data
+ size_hit='' # will not be consolidated up
+
+ done
+ last_entry "$size_hit" "$pline" "$repack_line"
+
+ [ $sum -gt 0 ] && echo "c$run - $sum [$(outfs , "${clist[@]}")] - - -"
+
+ } | sort_list
+}
+
+process_list() { # git.repo > list
+ local list=$(init_list "$1") plist run=0
+
+ while true ; do
+ plist=$list
+ run=$((run +1))
+ list=$(echo "$list" | consolidate_list "$run")
+ if [ "$plist" != "$list" ] ; then
+ debug "------------------------------------------------------------------------------------"
+ debug "$HEADER"
+ debug "$list"
+ else
+ break
+ fi
+ done
+ debug "------------------------------------------------------------------------------------"
+ echo "$list"
+}
+
+repack_list() { # git.repo < list
+ local repo=$1
+ local start_date newpacks=0 pkeeps keeps=1 refs refdirs rtn
+ local packedrefs=$(<"$repo/packed-refs")
+
+ # so they don't appear touched after a noop refpacking
+ if [ -z "$SW_REFS" ] ; then
+ refs=$(cd "$repo/refs" ; find -depth)
+ refdirs=$(cd "$repo/refs" ; find -type d -depth)
+ debug "Before refs:"
+ debug "$refs"
+ fi
+
+ # Find a private keep snapshot which has not changed from
+ # before our start_date so private keep deletions during gc
+ # can be detected
+ while ! array_equals pkeeps "${keeps[@]}" ; do
+ debug "Getting a private keep snapshot"
+ private_keeps "$repo"
+ keeps=("${pkeeps[@]}")
+ debug "before keeps: ${keeps[*]}"
+ start_date=$(date)
+ private_keeps "$repo"
+ debug "after keeps: ${pkeeps[*]}"
+ done
+
+ while read n has_keep size sha repack down up note; do
+ if [ "$repack" = "y" ] ; then
+ keep="$repo/objects/pack/pack-$sha.keep"
+ info "Repacking $repo/objects/pack/pack-$sha.pack"
+ [ -f "$keep" ] && rm -f "$keep"
+ fi
+ done
+
+ ( cd "$repo" && git gc "${GC_OPTS[@]}" ) ; rtn=$?
+
+ # Mark any files withoug a .keep with our .keep
+ packs=("$repo"/objects/pack/pack-$SHA1.pack)
+ for pack in "${packs[@]}" ; do
+ if keep "$pack" ; then
+ info "New pack: $pack"
+ newpacks=$((newpacks+1))
+ fi
+ done
+
+ # Record start_time. If there is more than 1 new packfile, we
+ # don't want to risk touching it with an older date since that
+ # would prevent consolidation on the next run. If the private
+ # keeps have changed, then we should run next time no matter what.
+ if [ $newpacks -le 1 ] || ! array_equals pkeeps "${keeps[@]}" ; then
+ set_start_date "$repo" "$start_date" "$refs" "$refdirs" "$packedrefs" "${packs[@]}"
+ fi
+
+ return $rtn # we really only care about the gc error code
+}
+
+git_gc() { # git.repo
+ local list=$(process_list "$1")
+ if [ -z "$SW_V" ] ; then
+ info "Running $PROG on $1. git gc options: ${GC_OPTS[@]}"
+ echo "$HEADER" >&2
+ echo "$list" >&2 ;
+ fi
+ echo "$list" | repack_list "$1"
+}
+
+
+PROG=$(basename "$0")
+HEADER="Id Keep Size Sha1(or consolidation list) Actions(repack down up note)"
+KEEP=git-exproll
+HEX='[0-9a-f]'
+HEX10=$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX
+SHA1=$HEX10$HEX10$HEX10$HEX10
+
+RATIO=10
+SW_N='' ; SW_V='' ; SW_T='' ; SW_REFS='' ; SW_LOOSE='' ; GC_OPTS=()
+while [ $# -gt 0 ] ; do
+ case "$1" in
+ -u|-h) usage ;;
+ -n) SW_N="$1" ;;
+ -v) SW_V="$1" ;;
+
+ -t) SW_T="$1" ;;
+ --norefs) SW_REFS="$1" ;;
+ --noloose) SW_LOOSE="$1" ;;
+
+ -r|--ratio) shift ; RATIO="$1" ;;
+
+ *) [ $# -le 1 ] && break
+ GC_OPTS=( "${GC_OPTS[@]}" "$1" )
+ ;;
+ esac
+ shift
+done
+
+
+REPO="$1"
+if ! is_repo "$REPO" ; then
+ REPO=$REPO/.git
+ is_repo "$REPO" || usage "($1) is not likely a git repo"
+fi
+
+
+if [ -z "$SW_N" ] ; then
+ is_touched "$REPO" || { info "Repo untouched since last run" ; exit ; }
+ git_gc "$REPO"
+else
+ is_touched "$REPO" || info "Repo untouched since last run, analyze anyway."
+ process_list "$REPO" >&2
+fi
diff --git a/gerrit-antlr/.gitignore b/gerrit-antlr/.gitignore
index 194bedcbc4..fb047af030 100644
--- a/gerrit-antlr/.gitignore
+++ b/gerrit-antlr/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-antlr.iml \ No newline at end of file
diff --git a/gerrit-antlr/.settings/org.eclipse.core.resources.prefs b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
index 589908f32c..e9441bb123 100644
--- a/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index aa0d7fd127..34cb46f34a 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-cache-h2/.gitignore b/gerrit-cache-h2/.gitignore
new file mode 100644
index 0000000000..cb430b8d67
--- /dev/null
+++ b/gerrit-cache-h2/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-cache-h2.iml
diff --git a/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..f9fe34593f
--- /dev/null
+++ b/gerrit-cache-h2/.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-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
index 8667cfd4a3..8667cfd4a3 100644
--- a/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
+++ b/gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
index e89c0486b2..470942d4f6 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Thu Jan 19 12:55:44 PST 2012
+#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
index d4218a5fc0..d4218a5fc0 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
+++ b/gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
diff --git a/gerrit-ehcache/pom.xml b/gerrit-cache-h2/pom.xml
index 839c52b063..4d4303c2e0 100644
--- a/gerrit-ehcache/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
-Copyright (C) 2010 The Android Open Source Project
+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.
@@ -22,26 +22,31 @@ limitations under the License.
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
- <artifactId>gerrit-ehcache</artifactId>
- <name>Gerrit Code Review - Ehcache Bindings</name>
+ <artifactId>gerrit-cache-h2</artifactId>
+ <name>Gerrit Code Review - Guava + H2 caching</name>
<description>
- Bindings to Ehcache
+ Implementation of caching backed by Guava and H2
</description>
<dependencies>
<dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache-core</artifactId>
- </dependency>
-
- <dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-server</artifactId>
<version>${project.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
new file mode 100644
index 0000000000..5a600c0025
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -0,0 +1,135 @@
+// 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.cache.h2;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.ForwardingRemovalListener;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.TimeUnit;
+
+public class DefaultCacheFactory implements MemoryCacheFactory {
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ install(new FactoryModule() {
+ @Override
+ protected void configure() {
+ factory(ForwardingRemovalListener.Factory.class);
+ }
+ });
+
+ bind(DefaultCacheFactory.class);
+ bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
+ bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+ listener().to(H2CacheFactory.class);
+ }
+ }
+
+ private final Config cfg;
+ private final ForwardingRemovalListener.Factory forwardingRemovalListenerFactory;
+
+ @Inject
+ public DefaultCacheFactory(@GerritServerConfig Config config,
+ ForwardingRemovalListener.Factory forwardingRemovalListenerFactory) {
+ this.cfg = config;
+ this.forwardingRemovalListenerFactory = forwardingRemovalListenerFactory;
+ }
+
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ return create(def, false).build();
+ }
+
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ return create(def, false).build(loader);
+ }
+
+ @SuppressWarnings("unchecked")
+ <K, V> CacheBuilder<K, V> create(
+ CacheBinding<K, V> def,
+ boolean unwrapValueHolder) {
+ CacheBuilder<K,V> builder = newCacheBuilder();
+ builder.recordStats();
+ builder.maximumWeight(cfg.getLong(
+ "cache", def.name(), "memoryLimit",
+ def.maximumWeight()));
+
+ builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
+
+ Weigher<K, V> weigher = def.weigher();
+ if (weigher != null && unwrapValueHolder) {
+ final Weigher<K, V> impl = weigher;
+ weigher = (Weigher<K, V>) new Weigher<K, ValueHolder<V>> () {
+ @Override
+ public int weigh(K key, ValueHolder<V> value) {
+ return impl.weigh(key, value.value);
+ }
+ };
+ } else if (weigher == null) {
+ weigher = unitWeight();
+ }
+ builder.weigher(weigher);
+
+ Long age = def.expireAfterWrite(TimeUnit.SECONDS);
+ if (has(def.name(), "maxAge")) {
+ builder.expireAfterWrite(ConfigUtil.getTimeUnit(cfg,
+ "cache", def.name(), "maxAge",
+ age != null ? age : 0,
+ TimeUnit.SECONDS), TimeUnit.SECONDS);
+ } else if (age != null) {
+ builder.expireAfterWrite(age, TimeUnit.SECONDS);
+ }
+
+ return builder;
+ }
+
+ private boolean has(String name, String var) {
+ return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static <K, V> CacheBuilder<K, V> newCacheBuilder() {
+ CacheBuilder builder = CacheBuilder.newBuilder();
+ return builder;
+ }
+
+ private static <K, V> Weigher<K, V> unitWeight() {
+ return new Weigher<K, V>() {
+ @Override
+ public int weigh(K key, V value) {
+ return 1;
+ }
+ };
+ }
+}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
new file mode 100644
index 0000000000..27da20fa90
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -0,0 +1,198 @@
+// 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.cache.h2;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
+ static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+
+ private final DefaultCacheFactory defaultFactory;
+ private final Config config;
+ private final File cacheDir;
+ private final List<H2CacheImpl<?, ?>> caches;
+ private final ExecutorService executor;
+ private final ScheduledExecutorService cleanup;
+ private volatile boolean started;
+
+ @Inject
+ H2CacheFactory(
+ DefaultCacheFactory defaultCacheFactory,
+ @GerritServerConfig Config cfg,
+ SitePaths site) {
+ defaultFactory = defaultCacheFactory;
+ config = cfg;
+
+ File loc = site.resolve(cfg.getString("cache", null, "directory"));
+ if (loc == null) {
+ cacheDir = null;
+ } else if (loc.exists() || loc.mkdirs()) {
+ if (loc.canWrite()) {
+ log.info("Enabling disk cache " + loc.getAbsolutePath());
+ cacheDir = loc;
+ } else {
+ log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+ } else {
+ log.warn("Can't create disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+
+ caches = Lists.newLinkedList();
+
+ if (cacheDir != null) {
+ executor = Executors.newFixedThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Store-%d")
+ .build());
+ cleanup = Executors.newScheduledThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Prune-%d")
+ .setDaemon(true)
+ .build());
+ } else {
+ executor = null;
+ cleanup = null;
+ }
+ }
+
+ @Override
+ public void start() {
+ started = true;
+ if (executor != null) {
+ for (final H2CacheImpl<?, ?> cache : caches) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ cache.start();
+ }
+ });
+
+ cleanup.schedule(new Runnable() {
+ @Override
+ public void run() {
+ cache.prune(cleanup);
+ }
+ }, 30, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (executor != null) {
+ try {
+ cleanup.shutdownNow();
+
+ List<Runnable> pending = executor.shutdownNow();
+ if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
+ if (pending != null && !pending.isEmpty()) {
+ log.info(String.format("Finishing %d disk cache updates", pending.size()));
+ for (Runnable update : pending) {
+ update.run();
+ }
+ }
+ } else {
+ log.info("Timeout waiting for disk cache to close");
+ }
+ } catch (InterruptedException e) {
+ log.warn("Interrupted waiting for disk cache to shutdown");
+ }
+ }
+ for (H2CacheImpl<?, ?> cache : caches) {
+ cache.stop();
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes", "cast"})
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(),
+ (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
+ caches.add(cache);
+ return cache;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def, loader);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
+ defaultFactory.create(def, true)
+ .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
+ executor, store, loader));
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(), mem);
+ caches.add(cache);
+ return cache;
+ }
+
+ private <V, K> SqlStore<K, V> newSqlStore(
+ String name,
+ TypeLiteral<K> keyType,
+ long maxSize) {
+ File db = new File(cacheDir, name).getAbsoluteFile();
+ String url = "jdbc:h2:" + db.toURI().toString();
+ return new SqlStore<K, V>(url, keyType, maxSize);
+ }
+}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
new file mode 100644
index 0000000000..a196b07054
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -0,0 +1,726 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.cache.AbstractLoadingCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.PrimitiveSink;
+import com.google.inject.TypeLiteral;
+
+import org.h2.jdbc.JdbcSQLException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InvalidClassException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hybrid in-memory and database backed cache built on H2.
+ * <p>
+ * This cache can be used as either a recall cache, or a loading cache if a
+ * CacheLoader was supplied to its constructor at build time. Before creating an
+ * entry the in-memory cache is checked for the item, then the database is
+ * checked, and finally the CacheLoader is used to construct the item. This is
+ * mostly useful for CacheLoaders that are computationally intensive, such as
+ * the PatchListCache.
+ * <p>
+ * Cache stores and invalidations are performed on a background thread, hiding
+ * the latency associated with serializing the key and value pairs and writing
+ * them to the database log.
+ * <p>
+ * A BloomFilter is used around the database to reduce the number of SELECTs
+ * issued against the database for new cache items that have not been seen
+ * before, a common operation for the PatchListCache. The BloomFilter is sized
+ * when the cache starts to be 64,000 entries or double the number of items
+ * currently in the database table.
+ * <p>
+ * This cache does not export its items as a ConcurrentMap.
+ *
+ * @see H2CacheFactory
+ */
+public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
+ private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
+
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final TypeLiteral<K> keyType;
+ private final Cache<K, ValueHolder<V>> mem;
+
+ H2CacheImpl(Executor executor,
+ SqlStore<K, V> store,
+ TypeLiteral<K> keyType,
+ Cache<K, ValueHolder<V>> mem) {
+ this.executor = executor;
+ this.store = store;
+ this.keyType = keyType;
+ this.mem = mem;
+ }
+
+ @Override
+ public V getIfPresent(Object objKey) {
+ if (!keyType.getRawType().isInstance(objKey)) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ K key = (K) objKey;
+
+ ValueHolder<V> h = mem.getIfPresent(key);
+ if (h != null) {
+ return h.value;
+ }
+
+ if (store.mightContain(key)) {
+ h = store.getIfPresent(key);
+ if (h != null) {
+ mem.put(key, h);
+ return h.value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public V get(K key) throws ExecutionException {
+ if (mem instanceof LoadingCache) {
+ return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void put(final K key, V val) {
+ final ValueHolder<V> h = new ValueHolder<V>(val);
+ h.created = System.currentTimeMillis();
+ mem.put(key, h);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void invalidate(final Object key) {
+ if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.invalidate((K) key);
+ }
+ });
+ }
+ mem.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ store.invalidateAll();
+ mem.invalidateAll();
+ }
+
+ @Override
+ public long size() {
+ return mem.size();
+ }
+
+ @Override
+ public CacheStats stats() {
+ return mem.stats();
+ }
+
+ public DiskStats diskStats() {
+ return store.diskStats();
+ }
+
+ void start() {
+ store.open();
+ }
+
+ void stop() {
+ for (Map.Entry<K, ValueHolder<V>> e : mem.asMap().entrySet()) {
+ ValueHolder<V> h = e.getValue();
+ if (!h.clean) {
+ store.put(e.getKey(), h);
+ }
+ }
+ store.close();
+ }
+
+ void prune(final ScheduledExecutorService service) {
+ store.prune(mem);
+
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 01);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DAY_OF_MONTH, 1);
+
+ long delay = cal.getTimeInMillis() - System.currentTimeMillis();
+ service.schedule(new Runnable() {
+ @Override
+ public void run() {
+ prune(service);
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ }
+
+ public static class DiskStats {
+ long size;
+ long space;
+ long hitCount;
+ long missCount;
+
+ public long size() {
+ return size;
+ }
+
+ public long space() {
+ return space;
+ }
+
+ public long hitCount() {
+ return hitCount;
+ }
+
+ public long requestCount() {
+ return hitCount + missCount;
+ }
+ }
+
+ static class ValueHolder<V> {
+ final V value;
+ long created;
+ volatile boolean clean;
+
+ ValueHolder(V value) {
+ this.value = value;
+ }
+ }
+
+ static class Loader<K, V> extends CacheLoader<K, ValueHolder<V>> {
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final CacheLoader<K, V> loader;
+
+ Loader(Executor executor, SqlStore<K, V> store, CacheLoader<K, V> loader) {
+ this.executor = executor;
+ this.store = store;
+ this.loader = loader;
+ }
+
+ @Override
+ public ValueHolder<V> load(final K key) throws Exception {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
+ h.created = System.currentTimeMillis();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ return h;
+ }
+ }
+
+ private static class KeyType<K> {
+ String columnType() {
+ return "OTHER";
+ }
+
+ @SuppressWarnings("unchecked")
+ K get(ResultSet rs, int col) throws SQLException {
+ return (K) rs.getObject(col);
+ }
+
+ void set(PreparedStatement ps, int col, K value) throws SQLException {
+ ps.setObject(col, value);
+ }
+
+ Funnel<K> funnel() {
+ return new Funnel<K>() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void funnel(K from, PrimitiveSink into) {
+ try {
+ ObjectOutputStream ser =
+ new ObjectOutputStream(new SinkOutputStream(into));
+ try {
+ ser.writeObject(from);
+ ser.flush();
+ } finally {
+ ser.close();
+ }
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot hash as Serializable", err);
+ }
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ static <K> KeyType<K> create(TypeLiteral<K> type) {
+ if (type.getRawType() == String.class) {
+ return (KeyType<K>) STRING;
+ }
+ return (KeyType<K>) OTHER;
+ }
+
+ static final KeyType<?> OTHER = new KeyType<Object>();
+ static final KeyType<String> STRING = new KeyType<String>() {
+ @Override
+ String columnType() {
+ return "VARCHAR(4096)";
+ }
+
+ @Override
+ String get(ResultSet rs, int col) throws SQLException {
+ return rs.getString(col);
+ }
+
+ @Override
+ void set(PreparedStatement ps, int col, String value)
+ throws SQLException {
+ ps.setString(col, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ Funnel<String> funnel() {
+ Funnel<?> s = Funnels.stringFunnel();
+ return (Funnel<String>) s;
+ }
+ };
+ }
+
+ static class SqlStore<K, V> {
+ private final String url;
+ private final KeyType<K> keyType;
+ private final long maxSize;
+ private final BlockingQueue<SqlHandle> handles;
+ private final AtomicLong hitCount = new AtomicLong();
+ private final AtomicLong missCount = new AtomicLong();
+ private volatile BloomFilter<K> bloomFilter;
+ private int estimatedSize;
+
+ SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize) {
+ this.url = jdbcUrl;
+ this.keyType = KeyType.create(keyType);
+ this.maxSize = maxSize;
+
+ int cores = Runtime.getRuntime().availableProcessors();
+ int keep = Math.min(cores, 16);
+ this.handles = new ArrayBlockingQueue<SqlHandle>(keep);
+ }
+
+ synchronized void open() {
+ if (bloomFilter == null) {
+ bloomFilter = buildBloomFilter();
+ }
+ }
+
+ void close() {
+ SqlHandle h;
+ while ((h = handles.poll()) != null) {
+ h.close();
+ }
+ }
+
+ boolean mightContain(K key) {
+ BloomFilter<K> b = bloomFilter;
+ if (b == null) {
+ synchronized (this) {
+ b = bloomFilter;
+ if (b == null) {
+ b = buildBloomFilter();
+ bloomFilter = b;
+ }
+ }
+ }
+ return b == null || b.mightContain(key);
+ }
+
+ private BloomFilter<K> buildBloomFilter() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r;
+ if (estimatedSize <= 0) {
+ r = s.executeQuery("SELECT COUNT(*) FROM data");
+ try {
+ estimatedSize = r.next() ? r.getInt(1) : 0;
+ } finally {
+ r.close();
+ }
+ }
+
+ BloomFilter<K> b = newBloomFilter();
+ r = s.executeQuery("SELECT k FROM data");
+ try {
+ while (r.next()) {
+ b.put(keyType.get(r, 1));
+ }
+ } catch (JdbcSQLException e) {
+ if (e.getCause() instanceof InvalidClassException) {
+ log.warn("Entries cached for " + url
+ + " have an incompatible class and can't be deserialized. "
+ + "Cache is flushed.");
+ invalidateAll();
+ } else {
+ throw e;
+ }
+ } finally {
+ r.close();
+ }
+ return b;
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot build BloomFilter for " + url, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ ValueHolder<V> getIfPresent(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.get == null) {
+ c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+ }
+ keyType.set(c.get, 1, key);
+ ResultSet r = c.get.executeQuery();
+ try {
+ if (!r.next()) {
+ missCount.incrementAndGet();
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ V val = (V) r.getObject(1);
+ ValueHolder<V> h = new ValueHolder<V>(val);
+ h.clean = true;
+ hitCount.incrementAndGet();
+ touch(c, key);
+ return h;
+ } finally {
+ r.close();
+ c.get.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot read cache " + url + " for " + key, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ private void touch(SqlHandle c, K key) throws SQLException {
+ if (c.touch == null) {
+ c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+ }
+ try {
+ c.touch.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
+ keyType.set(c.touch, 2, key);
+ c.touch.executeUpdate();
+ } finally {
+ c.touch.clearParameters();
+ }
+ }
+
+ void put(K key, ValueHolder<V> holder) {
+ if (holder.clean) {
+ return;
+ }
+
+ BloomFilter<K> b = bloomFilter;
+ if (b != null) {
+ b.put(key);
+ bloomFilter = b;
+ }
+
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.put == null) {
+ c.put = c.conn.prepareStatement("MERGE INTO data VALUES(?,?,?,?)");
+ }
+ try {
+ keyType.set(c.put, 1, key);
+ c.put.setObject(2, holder.value);
+ c.put.setTimestamp(3, new Timestamp(holder.created));
+ c.put.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+ c.put.executeUpdate();
+ holder.clean = true;
+ } finally {
+ c.put.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot put into cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void invalidate(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ invalidate(c, key);
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ private void invalidate(SqlHandle c, K key) throws SQLException {
+ if (c.invalidate == null) {
+ c.invalidate = c.conn.prepareStatement("DELETE FROM data WHERE k=?");
+ }
+ try {
+ keyType.set(c.invalidate, 1, key);
+ c.invalidate.executeUpdate();
+ } finally {
+ c.invalidate.clearParameters();
+ }
+ }
+
+ void invalidateAll() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ s.executeUpdate("DELETE FROM data");
+ } finally {
+ s.close();
+ }
+ bloomFilter = newBloomFilter();
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void prune(Cache<K, ?> mem) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ long used = 0;
+ ResultSet r = s.executeQuery("SELECT"
+ + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ used = r.next() ? r.getLong(1) : 0;
+ } finally {
+ r.close();
+ }
+ if (used <= maxSize) {
+ return;
+ }
+
+ r = s.executeQuery("SELECT"
+ + " k"
+ + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+ + " FROM data"
+ + " ORDER BY accessed");
+ try {
+ while (maxSize < used && r.next()) {
+ K key = keyType.get(r, 1);
+ if (mem.getIfPresent(key) != null) {
+ touch(c, key);
+ } else {
+ invalidate(c, key);
+ used -= r.getLong(2);
+ }
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot prune cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ DiskStats diskStats() {
+ DiskStats d = new DiskStats();
+ d.hitCount = hitCount.get();
+ d.missCount = missCount.get();
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r = s.executeQuery("SELECT"
+ + " COUNT(*)"
+ + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ if (r.next()) {
+ d.size = r.getLong(1);
+ d.space = r.getLong(2);
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot get DiskStats for " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ return d;
+ }
+
+ private SqlHandle acquire() throws SQLException {
+ SqlHandle h = handles.poll();
+ return h != null ? h : new SqlHandle(url, keyType);
+ }
+
+ private void release(SqlHandle h) {
+ if (h != null && !handles.offer(h)) {
+ h.close();
+ }
+ }
+
+ private SqlHandle close(SqlHandle h) {
+ if (h != null) {
+ h.close();
+ }
+ return null;
+ }
+
+ private BloomFilter<K> newBloomFilter() {
+ int cnt = Math.max(64 * 1024, 2 * estimatedSize);
+ return BloomFilter.create(keyType.funnel(), cnt);
+ }
+ }
+
+ static class SqlHandle {
+ private final String url;
+ Connection conn;
+ PreparedStatement get;
+ PreparedStatement put;
+ PreparedStatement touch;
+ PreparedStatement invalidate;
+
+ SqlHandle(String url, KeyType<?> type) throws SQLException {
+ this.url = url;
+ this.conn = org.h2.Driver.load().connect(url, null);
+ Statement stmt = conn.createStatement();
+ try {
+ stmt.execute("CREATE TABLE IF NOT EXISTS data"
+ + "(k " + type.columnType() + " NOT NULL PRIMARY KEY HASH"
+ + ",v OTHER NOT NULL"
+ + ",created TIMESTAMP NOT NULL"
+ + ",accessed TIMESTAMP NOT NULL"
+ + ")");
+ } finally {
+ stmt.close();
+ }
+ }
+
+ void close() {
+ get = closeStatement(get);
+ put = closeStatement(put);
+ touch = closeStatement(touch);
+ invalidate = closeStatement(invalidate);
+
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close connection to " + url, e);
+ } finally {
+ conn = null;
+ }
+ }
+ }
+
+ private PreparedStatement closeStatement(PreparedStatement ps) {
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close statement for " + url, e);
+ }
+ }
+ return null;
+ }
+ }
+
+ private static class SinkOutputStream extends OutputStream {
+ private final PrimitiveSink sink;
+
+ SinkOutputStream(PrimitiveSink sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void write(int b) {
+ sink.putByte((byte)b);
+ }
+
+ @Override
+ public void write(byte[] b, int p, int n) {
+ sink.putBytes(b, p, n);
+ }
+ }
+}
diff --git a/gerrit-common/.gitignore b/gerrit-common/.gitignore
index 194bedcbc4..759f12cc06 100644
--- a/gerrit-common/.gitignore
+++ b/gerrit-common/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-common.iml \ No newline at end of file
diff --git a/gerrit-common/.settings/org.eclipse.core.resources.prefs b/gerrit-common/.settings/org.eclipse.core.resources.prefs
index fc11c3fe6f..f9fe34593f 100644
--- a/gerrit-common/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-common/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e7933eaa89..9b3fe5f659 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-common</artifactId>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index 71df40057f..2b9b72a61b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -14,13 +14,11 @@
package com.google.gerrit.common;
-import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gwtorm.client.KeyUtil;
public class PageLinks {
@@ -40,8 +38,10 @@ public class PageLinks {
public static final String MINE = "/";
public static final String ADMIN_GROUPS = "/admin/groups/";
+ public static final String ADMIN_CREATE_GROUP = "/admin/create-group/";
public static final String ADMIN_PROJECTS = "/admin/projects/";
public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
+ public static final String ADMIN_PLUGINS = "/admin/plugins/";
public static String toChange(final ChangeInfo c) {
return toChange(c.getId());
@@ -59,12 +59,12 @@ public class PageLinks {
return "/admin/projects/" + p.get() + ",access";
}
- public static String toAccountDashboard(final AccountInfo acct) {
- return toAccountDashboard(acct.getId());
+ public static String toAccountQuery(final String fullname) {
+ return toAccountQuery(fullname, Status.NEW);
}
- public static String toAccountDashboard(final Account.Id acct) {
- return "/dashboard/" + acct.toString();
+ public static String toAccountQuery(String fullname, Status status) {
+ return toChangeQuery(op("owner", fullname) + " " + status(status), TOP);
}
public static String toChangeQuery(final String query) {
@@ -72,30 +72,38 @@ public class PageLinks {
}
public static String toChangeQuery(String query, String page) {
- query = KeyUtil.encode(query).replaceAll("%3[Aa]", ":");
- return "/q/" + query + "," + page;
+ return "/q/" + KeyUtil.encode(query) + "," + page;
}
public static String projectQuery(Project.NameKey proj, Status status) {
+ return status(status) + " " + op("project", proj.get());
+ }
+
+ private static String status(Status status) {
switch (status) {
case ABANDONED:
- return "status:abandoned " + op("project", proj.get());
-
+ return "status:abandoned";
case MERGED:
- return "status:merged " + op("project", proj.get());
-
+ return "status:merged";
case NEW:
case SUBMITTED:
default:
- return "status:open " + op("project", proj.get());
+ return "status:open";
+ }
+ }
+
+ public static String op(String op, String value) {
+ if (isSingleWord(value)) {
+ return op + ":" + value;
}
+ return op + ":\"" + value + "\"";
}
- public static String op(String name, String value) {
- if (value.indexOf(' ') >= 0) {
- return name + ":\"" + value + "\"";
+ private static boolean isSingleWord(String value) {
+ if (value.startsWith("-")) {
+ return false;
}
- return name + ":" + value;
+ return value.matches("[^\u0000-\u0020!\"#$%&'():;?\\[\\]{}~]+");
}
protected PageLinks() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java b/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java
new file mode 100644
index 0000000000..90c7f75165
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java
@@ -0,0 +1,36 @@
+// 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.common.audit;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Audit annotation for JSON/RPC interfaces.
+ *
+ * Flag with @Audit all the JSON/RPC methods to
+ * be traced in audit-trail and submitted to the
+ * AuditService.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Audit {
+ String action() default "";
+
+ /** List of positions of parameters to be obfuscated in audit-trail (i.e. passwords) */
+ int[] obfuscate() default {};
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
index 1d25a3dff1..0936d23242 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.auth.userpass;
+import com.google.gerrit.common.audit.Audit;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -22,6 +23,7 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0)
public interface UserPassAuthService extends RemoteJsonService {
+ @Audit(action = "sign in", obfuscate={1})
@AllowCrossSiteRequest
void authenticate(String username, String password,
AsyncCallback<LoginResult> callback);
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
new file mode 100644
index 0000000000..a5ab851ae8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -0,0 +1,73 @@
+// 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.common.changes;
+
+import java.util.EnumSet;
+
+/** Output options available when using {@code /changes/} RPCs. */
+public enum ListChangesOption {
+ LABELS(0),
+
+ /** Return information on the current patch set of the change. */
+ CURRENT_REVISION(1),
+ ALL_REVISIONS(2),
+
+ /** If revisions are included, parse the commit object. */
+ CURRENT_COMMIT(3),
+ ALL_COMMITS(4),
+
+ /** If a patch set is included, include the files of the patch set. */
+ CURRENT_FILES(5),
+ ALL_FILES(6);
+
+ private final int value;
+
+ private ListChangesOption(int v) {
+ this.value = v;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public static ListChangesOption fromValue(int value) {
+ return ListChangesOption.values()[value];
+ }
+
+ public static EnumSet<ListChangesOption> fromBits(int v) {
+ EnumSet<ListChangesOption> r = EnumSet.noneOf(ListChangesOption.class);
+ for (ListChangesOption o : ListChangesOption.values()) {
+ if ((v & (1 << o.value)) != 0) {
+ r.add(o);
+ v &= ~(1 << o.value);
+ }
+ if (v == 0) {
+ return r;
+ }
+ }
+ if (v != 0) {
+ throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
+ }
+ return r;
+ }
+
+ public static int toBits(EnumSet<ListChangesOption> set) {
+ int r = 0;
+ for (ListChangesOption o : set) {
+ r |= 1 << o.value;
+ }
+ return r;
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
index cd64b0abd0..a9b5e85503 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -118,4 +118,13 @@ public class AccessSection extends RefConfigSection implements
public String toString() {
return "AccessSection[" + getName() + "]";
}
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!super.equals(obj) || !(obj instanceof AccessSection)) {
+ return false;
+ }
+ return new HashSet<Permission>(permissions).equals(new HashSet<Permission>(
+ ((AccessSection) obj).permissions));
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 9a4d9fb0a9..92c2d6c8ed 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -65,4 +65,51 @@ public class AccountInfo {
public void setPreferredEmail(final String email) {
preferredEmail = email;
}
+
+ /**
+ * Formats an account name.
+ * <p>
+ * If the account has a full name, it returns only the full name. Otherwise it
+ * returns a longer form that includes the email address.
+ */
+ public String getName(String anonymousCowardName) {
+ if (getFullName() != null) {
+ return getFullName();
+ }
+ if (getPreferredEmail() != null) {
+ return getPreferredEmail();
+ }
+ return getNameEmail(anonymousCowardName);
+ }
+
+ /**
+ * Formats an account as an name and an email address.
+ * <p>
+ * Example output:
+ * <ul>
+ * <li><code>A U. Thor &lt;author@example.com&gt;</code>: full populated</li>
+ * <li><code>A U. Thor (12)</code>: missing email address</li>
+ * <li><code>Anonymous Coward &lt;author@example.com&gt;</code>: missing name</li>
+ * <li><code>Anonymous Coward (12)</code>: missing name and email address</li>
+ * </ul>
+ */
+ public String getNameEmail(String anonymousCowardName) {
+ String name = getFullName();
+ if (name == null) {
+ name = anonymousCowardName;
+ }
+
+ final StringBuilder b = new StringBuilder();
+ b.append(name);
+ if (getPreferredEmail() != null) {
+ b.append(" <");
+ b.append(getPreferredEmail());
+ b.append(">");
+ } else if (getId() != null) {
+ b.append(" (");
+ b.append(getId().get());
+ b.append(")");
+ }
+ return b.toString();
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 21aca692c5..aa212f9bb1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -14,17 +14,18 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService;
import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtjsonrpc.common.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.VoidResult;
import java.util.List;
import java.util.Set;
@@ -34,20 +35,25 @@ public interface AccountSecurity extends RemoteJsonService {
@SignInRequired
void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
+ @Audit
@SignInRequired
void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
+ @Audit
@SignInRequired
void deleteSshKeys(Set<AccountSshKey.Id> ids,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void changeUserName(String newName, AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void generatePassword(AccountExternalId.Key key,
AsyncCallback<AccountExternalId> callback);
+ @Audit
@SignInRequired
void clearPassword(AccountExternalId.Key key,
AsyncCallback<AccountExternalId> gerritCallback);
@@ -56,23 +62,28 @@ public interface AccountSecurity extends RemoteJsonService {
void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
@SignInRequired
- void myGroups(AsyncCallback<List<GroupDetail>> callback);
+ void myGroups(AsyncCallback<List<AccountGroup>> callback);
+ @Audit
@SignInRequired
void deleteExternalIds(Set<AccountExternalId.Key> keys,
AsyncCallback<Set<AccountExternalId.Key>> callback);
+ @Audit
@SignInRequired
void updateContact(String fullName, String emailAddr,
ContactInformation info, AsyncCallback<Account> callback);
+ @Audit
@SignInRequired
- void enterAgreement(ContributorAgreement.Id id,
+ void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void registerEmail(String address, AsyncCallback<Account> callback);
+ @Audit
@SignInRequired
void validateEmail(String token, AsyncCallback<VoidResult> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 7377d7e84f..18cf657345 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -36,10 +37,12 @@ public interface AccountService extends RemoteJsonService {
@SignInRequired
void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback);
+ @Audit
@SignInRequired
void changePreferences(AccountGeneralPreferences pref,
AsyncCallback<VoidResult> gerritCallback);
+ @Audit
@SignInRequired
void changeDiffPreferences(AccountDiffPreference diffPref,
AsyncCallback<VoidResult> callback);
@@ -47,14 +50,17 @@ public interface AccountService extends RemoteJsonService {
@SignInRequired
void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
+ @Audit
@SignInRequired
void addProjectWatch(String projectName, String filter,
AsyncCallback<AccountProjectWatchInfo> callback);
+ @Audit
@SignInRequired
void updateProjectWatch(AccountProjectWatch watch,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void deleteProjectWatches(Set<AccountProjectWatch.Key> keys,
AsyncCallback<VoidResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
index 0c6f6b76b6..7464bd1acf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
@@ -14,30 +14,21 @@
package com.google.gerrit.common.data;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-
import java.util.List;
import java.util.Map;
public class AgreementInfo {
- public List<AccountAgreement> userAccepted;
- public List<AccountGroupAgreement> groupAccepted;
- public Map<ContributorAgreement.Id, ContributorAgreement> agreements;
+ public List<String> accepted;
+ public Map<String, ContributorAgreement> agreements;
public AgreementInfo() {
}
- public void setUserAccepted(List<AccountAgreement> a) {
- userAccepted = a;
- }
-
- public void setGroupAccepted(List<AccountGroupAgreement> a) {
- groupAccepted = a;
+ public void setAccepted(List<String> a) {
+ accepted = a;
}
- public void setAgreements(Map<ContributorAgreement.Id, ContributorAgreement> a) {
+ public void setAgreements(Map<String, ContributorAgreement> a) {
agreements = a;
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 8b436242f0..c50d2e3edf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.Change;
@@ -25,12 +26,16 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0)
public interface ChangeDetailService extends RemoteJsonService {
+ @Audit
void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback);
+ @Audit
void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback);
+ @Audit
void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
+ @Audit
void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
index f646bc6f15..0c466497ad 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
@@ -14,39 +14,22 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService;
import com.google.gwtjsonrpc.common.RpcImpl;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import java.util.Set;
-
@RpcImpl(version = Version.V2_0)
public interface ChangeListService extends RemoteJsonService {
- /** Get all changes which match an arbitrary query string. */
- void allQueryPrev(String query, String pos, int limit,
- AsyncCallback<SingleListChangeInfo> callback);
-
- /** Get all changes which match an arbitrary query string. */
- void allQueryNext(String query, String pos, int limit,
- AsyncCallback<SingleListChangeInfo> callback);
-
- /** Get the data to show AccountDashboardScreen for an account. */
- void forAccount(Account.Id id, AsyncCallback<AccountDashboardInfo> callback);
-
- /** Get the ids of all changes starred by the caller. */
- @SignInRequired
- void myStarredChangeIds(AsyncCallback<Set<Change.Id>> callback);
-
/**
* Add and/or remove changes from the set of starred changes of the caller.
*
* @param req the add and remove cluster.
*/
+ @Audit
@SignInRequired
void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
index 872bddc5d0..4ef6b3e69c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -24,27 +25,34 @@ import com.google.gwtjsonrpc.common.RpcImpl.Version;
@RpcImpl(version = Version.V2_0)
public interface ChangeManageService extends RemoteJsonService {
+ @Audit
@SignInRequired
void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void abandonChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void revertChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void restoreChange(PatchSet.Id patchSetId, String message,
AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
new file mode 100644
index 0000000000..e02d9d377e
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -0,0 +1,111 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Portion of a {@link Project} describing a single contributor agreement. */
+public class ContributorAgreement implements Comparable<ContributorAgreement> {
+ protected String name;
+ protected String description;
+ protected List<PermissionRule> accepted;
+ protected boolean requireContactInformation;
+ protected GroupReference autoVerify;
+ protected String agreementUrl;
+
+ protected ContributorAgreement() {
+ }
+
+ public ContributorAgreement(String name) {
+ setName(name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public List<PermissionRule> getAccepted() {
+ if (accepted == null) {
+ accepted = new ArrayList<PermissionRule>();
+ }
+ return accepted;
+ }
+
+ public void setAccepted(List<PermissionRule> accepted) {
+ this.accepted = accepted;
+ }
+
+ public boolean isRequireContactInformation() {
+ return requireContactInformation;
+ }
+
+ public void setRequireContactInformation(boolean requireContactInformation) {
+ this.requireContactInformation = requireContactInformation;
+ }
+
+ public GroupReference getAutoVerify() {
+ return autoVerify;
+ }
+
+ public void setAutoVerify(GroupReference autoVerify) {
+ this.autoVerify = autoVerify;
+ }
+
+ public String getAgreementUrl() {
+ return agreementUrl;
+ }
+
+ public void setAgreementUrl(String agreementUrl) {
+ this.agreementUrl = agreementUrl;
+ }
+
+ @Override
+ public int compareTo(ContributorAgreement o) {
+ return getName().compareTo(o.getName());
+ }
+
+ @Override
+ public String toString() {
+ return "ContributorAgreement[" + getName() + "]";
+ }
+
+ public ContributorAgreement forUi() {
+ ContributorAgreement ca = new ContributorAgreement(name);
+ ca.description = description;
+ ca.accepted = Collections.emptyList();
+ ca.requireContactInformation = requireContactInformation;
+ if (autoVerify != null) {
+ ca.autoVerify = new GroupReference();
+ }
+ ca.agreementUrl = agreementUrl ;
+ return ca;
+ }
+}
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 456ffb4421..7c16129c14 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
@@ -16,9 +16,11 @@ package com.google.gerrit.common.data;
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+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.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gwtexpui.safehtml.client.RegexFindReplace;
import java.util.List;
@@ -27,6 +29,8 @@ import java.util.Set;
public class GerritConfig implements Cloneable {
protected String registerUrl;
protected String httpPasswordUrl;
+ protected String reportBugUrl;
+ protected String openIdSsoUrl;
protected List<OpenIdProviderPattern> allowedOpenIDs;
protected GitwebConfig gitweb;
@@ -35,9 +39,11 @@ public class GerritConfig implements Cloneable {
protected boolean allowRegisterNewEmail;
protected AuthType authType;
protected Set<DownloadScheme> downloadSchemes;
+ protected Set<DownloadCommand> downloadCommands;
protected String gitDaemonUrl;
protected String gitHttpUrl;
protected String sshdAddress;
+ protected String editFullNameUrl;
protected Project.NameKey wildProject;
protected ApprovalTypes approvalTypes;
protected Set<Account.FieldName> editableAccountFields;
@@ -54,6 +60,22 @@ public class GerritConfig implements Cloneable {
registerUrl = u;
}
+ public String getReportBugUrl() {
+ return reportBugUrl;
+ }
+
+ public void setReportBugUrl(String u) {
+ reportBugUrl = u;
+ }
+
+ public String getEditFullNameUrl() {
+ return editFullNameUrl;
+ }
+
+ public void setEditFullNameUrl(String u) {
+ editFullNameUrl = u;
+ }
+
public String getHttpPasswordUrl() {
return httpPasswordUrl;
}
@@ -62,6 +84,14 @@ public class GerritConfig implements Cloneable {
httpPasswordUrl = url;
}
+ public String getOpenIdSsoUrl() {
+ return openIdSsoUrl;
+ }
+
+ public void setOpenIdSsoUrl(final String u) {
+ openIdSsoUrl = u;
+ }
+
public List<OpenIdProviderPattern> getAllowedOpenIDs() {
return allowedOpenIDs;
}
@@ -86,6 +116,14 @@ public class GerritConfig implements Cloneable {
downloadSchemes = s;
}
+ public Set<DownloadCommand> getDownloadCommands() {
+ return downloadCommands;
+ }
+
+ public void setDownloadCommands(final Set<DownloadCommand> downloadCommands) {
+ this.downloadCommands = downloadCommands;
+ }
+
public GitwebConfig getGitwebLink() {
return gitweb;
}
@@ -199,4 +237,13 @@ public class GerritConfig implements Cloneable {
public void setAnonymousCowardName(final String anonymousCowardName) {
this.anonymousCowardName = anonymousCowardName;
}
+
+ public boolean siteHasUsernames() {
+ if (getAuthType() == AuthType.CUSTOM_EXTENSION
+ && getHttpPasswordUrl() != null
+ && !canEdit(FieldName.USER_NAME)) {
+ return false;
+ }
+ return true;
+ }
}
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 d3d2a4d71f..81d4fc9242 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
@@ -15,6 +15,8 @@
package com.google.gerrit.common.data;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
/** Server wide capabilities. Represented as {@link Permission} objects. */
@@ -40,12 +42,13 @@ public class GlobalCapability {
public static final String CREATE_PROJECT = "createProject";
/**
- * Denotes who may email change reviewers.
+ * Denotes who may email change reviewers and watchers.
* <p>
* This can be used to deny build bots from emailing reviewers and people who
- * have starred the changed. Instead, only the authors of the change will be
- * emailed. The allow rules are evaluated before deny rules, however the
- * default is to allow emailing, if no explicit rule is matched.
+ * watch the change. Instead, only the authors of the change and those who
+ * starred it will be emailed. The allow rules are evaluated before deny
+ * rules, however the default is to allow emailing, if no explicit rule is
+ * matched.
*/
public static final String EMAIL_REVIEWERS = "emailReviewers";
@@ -73,23 +76,34 @@ public class GlobalCapability {
/** Can view all pending tasks in the queue (not just the filtered set). */
public static final String VIEW_QUEUE = "viewQueue";
+ private static final List<String> NAMES_ALL;
private static final List<String> NAMES_LC;
static {
- NAMES_LC = new ArrayList<String>();
- NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase());
- NAMES_LC.add(CREATE_ACCOUNT.toLowerCase());
- NAMES_LC.add(CREATE_GROUP.toLowerCase());
- NAMES_LC.add(CREATE_PROJECT.toLowerCase());
- NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase());
- NAMES_LC.add(FLUSH_CACHES.toLowerCase());
- NAMES_LC.add(KILL_TASK.toLowerCase());
- NAMES_LC.add(PRIORITY.toLowerCase());
- NAMES_LC.add(QUERY_LIMIT.toLowerCase());
- NAMES_LC.add(START_REPLICATION.toLowerCase());
- NAMES_LC.add(VIEW_CACHES.toLowerCase());
- NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase());
- NAMES_LC.add(VIEW_QUEUE.toLowerCase());
+ NAMES_ALL = new ArrayList<String>();
+ NAMES_ALL.add(ADMINISTRATE_SERVER);
+ NAMES_ALL.add(CREATE_ACCOUNT);
+ NAMES_ALL.add(CREATE_GROUP);
+ NAMES_ALL.add(CREATE_PROJECT);
+ NAMES_ALL.add(EMAIL_REVIEWERS);
+ NAMES_ALL.add(FLUSH_CACHES);
+ NAMES_ALL.add(KILL_TASK);
+ NAMES_ALL.add(PRIORITY);
+ NAMES_ALL.add(QUERY_LIMIT);
+ NAMES_ALL.add(START_REPLICATION);
+ NAMES_ALL.add(VIEW_CACHES);
+ NAMES_ALL.add(VIEW_CONNECTIONS);
+ NAMES_ALL.add(VIEW_QUEUE);
+
+ NAMES_LC = new ArrayList<String>(NAMES_ALL.size());
+ for (String name : NAMES_ALL) {
+ NAMES_LC.add(name.toLowerCase());
+ }
+ }
+
+ /** @return all valid capability names. */
+ public static Collection<String> getAllNames() {
+ return Collections.unmodifiableList(NAMES_ALL);
}
/** @return true if the name is recognized as a capability name. */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index f385e27255..5cb7fa2f57 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
@@ -24,61 +25,64 @@ import com.google.gwtjsonrpc.common.RpcImpl;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import java.util.List;
import java.util.Set;
@RpcImpl(version = Version.V2_0)
public interface GroupAdminService extends RemoteJsonService {
+ @Audit
@SignInRequired
void visibleGroups(AsyncCallback<GroupList> callback);
+ @Audit
@SignInRequired
void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
+ @Audit
@SignInRequired
void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
AsyncCallback<GroupDetail> callback);
+ @Audit
@SignInRequired
void changeGroupDescription(AccountGroup.Id groupId, String description,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void renameGroup(AccountGroup.Id groupId, String newName,
AsyncCallback<GroupDetail> callback);
+ @Audit
@SignInRequired
void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
AsyncCallback<VoidResult> callback);
- @SignInRequired
- void changeExternalGroup(AccountGroup.Id groupId,
- AccountGroup.ExternalNameKey bindTo, AsyncCallback<VoidResult> callback);
-
- @SignInRequired
- void searchExternalGroups(String searchFilter,
- AsyncCallback<List<AccountGroup.ExternalNameKey>> callback);
-
+ @Audit
@SignInRequired
void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
AsyncCallback<GroupDetail> callback);
+ @Audit
@SignInRequired
void addGroupInclude(AccountGroup.Id groupId, String groupName,
AsyncCallback<GroupDetail> callback);
+ @Audit
@SignInRequired
void deleteGroupMembers(AccountGroup.Id groupId,
Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void deleteGroupIncludes(AccountGroup.Id groupId,
Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
new file mode 100644
index 0000000000..828bf24ac8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -0,0 +1,48 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/**
+ * Group methods exposed by the GroupBackend.
+ */
+public class GroupDescription {
+ /**
+ * The Basic information required to be exposed by any Group.
+ */
+ public interface Basic {
+ /** @return the non-null UUID of the group. */
+ AccountGroup.UUID getGroupUUID();
+
+ /** @return the non-null name of the group. */
+ String getName();
+
+ /** @return whether the group is visible to all accounts. */
+ boolean isVisibleToAll();
+ }
+
+ /**
+ * The extended information exposed by internal groups backed by an
+ * AccountGroup.
+ */
+ public interface Internal extends Basic {
+ /** @return the backing AccountGroup. */
+ AccountGroup getAccountGroup();
+ }
+
+ private GroupDescription() {
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
new file mode 100644
index 0000000000..e0bc7d816d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -0,0 +1,60 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for building GroupDescription objects.
+ */
+public class GroupDescriptions {
+
+ @Nullable
+ public static AccountGroup toAccountGroup(GroupDescription.Basic group) {
+ if (group instanceof GroupDescription.Internal) {
+ return ((GroupDescription.Internal) group).getAccountGroup();
+ }
+ return null;
+ }
+
+ public static GroupDescription.Internal forAccountGroup(final AccountGroup group) {
+ return new GroupDescription.Internal() {
+ @Override
+ public AccountGroup.UUID getGroupUUID() {
+ return group.getGroupUUID();
+ }
+
+ @Override
+ public String getName() {
+ return group.getName();
+ }
+
+ @Override
+ public boolean isVisibleToAll() {
+ return group.isVisibleToAll();
+ }
+
+ @Override
+ public AccountGroup getAccountGroup() {
+ return group;
+ }
+ };
+ }
+
+ private GroupDescriptions() {
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 65723f7d1e..01c7985c0e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -26,7 +26,7 @@ public class GroupDetail {
public AccountGroup group;
public List<AccountGroupMember> members;
public List<AccountGroupInclude> includes;
- public AccountGroup ownerGroup;
+ public GroupReference ownerGroup;
public boolean canModify;
public GroupDetail() {
@@ -52,7 +52,7 @@ public class GroupDetail {
includes = i;
}
- public void setOwnerGroup(AccountGroup g) {
+ public void setOwnerGroup(GroupReference g) {
ownerGroup = g;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
index 6352461a76..b3095cd20f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
@@ -14,25 +14,27 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
import java.util.List;
public class GroupList {
- protected List<GroupDetail> groups;
+ protected List<AccountGroup> groups;
protected boolean canCreateGroup;
protected GroupList() {
}
- public GroupList(final List<GroupDetail> groups, final boolean canCreateGroup) {
+ public GroupList(final List<AccountGroup> groups, final boolean canCreateGroup) {
this.groups = groups;
this.canCreateGroup = canCreateGroup;
}
- public List<GroupDetail> getGroups() {
+ public List<AccountGroup> getGroups() {
return groups;
}
- public void setGroups(List<GroupDetail> groups) {
+ public void setGroups(List<AccountGroup> groups) {
this.groups = groups;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
index f05d1b9007..c261fdd0a1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -23,6 +23,10 @@ public class GroupReference implements Comparable<GroupReference> {
return new GroupReference(group.getGroupUUID(), group.getName());
}
+ public static GroupReference forGroup(GroupDescription.Basic group) {
+ return new GroupReference(group.getGroupUUID(), group.getName());
+ }
+
protected String uuid;
protected String name;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index c3d3f1e4d2..f991f4c118 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -31,5 +31,8 @@ public class HostPageData {
public String textColor;
public String trimColor;
public String selectionColor;
+ public String changeTableOutdatedColor;
+ public String tableOddRowColor;
+ public String tableEvenRowColor;
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 68676cf9b5..2a70d6c973 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -15,6 +15,7 @@
package com.google.gerrit.common.data;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -62,28 +63,9 @@ public class ParameterizedString {
raw.append(pattern.substring(i, b));
ops.add(new Constant(pattern.substring(i, b)));
- String expr = pattern.substring(b + 2, e);
- String parameterName = "";
- List<Function> functions = new ArrayList<Function>();
- if (!expr.contains(".")) {
- parameterName = expr;
- } else {
- int firstDot = expr.indexOf('.');
- parameterName = expr.substring(0, firstDot);
- String actionsStr = expr.substring(firstDot + 1);
- String[] actions = actionsStr.split("\\.");
-
- for (String action : actions) {
- Function function = FUNCTIONS.get(action);
- if (function == null) {
- function = NOOP;
- }
- functions.add(function);
- }
- }
+ // "${parameter[.functions...]}" -> "parameter[.functions...]"
+ final Parameter p = new Parameter(pattern.substring(b + 2, e));
- final Parameter p =
- new Parameter(parameterName, Collections.unmodifiableList(functions));
raw.append("{" + prs.size() + "}");
prs.add(p);
ops.add(p);
@@ -184,9 +166,25 @@ public class ParameterizedString {
private final String name;
private final List<Function> functions;
- Parameter(final String name, final List<Function> functions) {
- this.name = name;
- this.functions = functions;
+ Parameter(final String parameter) {
+ // "parameter[.functions...]" -> (parameter, functions...)
+ final List<String> names = Arrays.asList(parameter.split("\\."));
+ final List<Function> functs = new ArrayList<Function>(names.size());
+
+ if (names.isEmpty()) {
+ name = "";
+ } else {
+ name = names.get(0);
+
+ for (String fname : names.subList(1, names.size())) {
+ final Function function = FUNCTIONS.get(fname);
+ if (function != null) {
+ functs.add(function);
+ }
+ }
+ }
+
+ functions = Collections.unmodifiableList(functs);
}
@Override
@@ -207,12 +205,6 @@ public class ParameterizedString {
}
private static final Map<String, Function> FUNCTIONS = initFunctions();
- private static final Function NOOP = new Function() {
- @Override
- String apply(String a) {
- return a;
- }
- };
private static Map<String, Function> initFunctions() {
final HashMap<String, Function> m = new HashMap<String, Function>();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
index 019154403e..91ecb92d34 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -34,13 +35,16 @@ import java.util.Set;
@RpcImpl(version = Version.V2_0)
public interface PatchDetailService extends RemoteJsonService {
+ @Audit
void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
+ @Audit
@SignInRequired
void saveDraft(PatchLineComment comment,
AsyncCallback<PatchLineComment> callback);
+ @Audit
@SignInRequired
void deleteDraft(PatchLineComment.Key key, AsyncCallback<VoidResult> callback);
@@ -57,18 +61,22 @@ public interface PatchDetailService extends RemoteJsonService {
* change, then <code>null</code> is passed as result to
* {@link AsyncCallback#onSuccess(Object)}
*/
+ @Audit
@SignInRequired
void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
+ @Audit
@SignInRequired
void publishComments(PatchSet.Id psid, String message,
Set<ApprovalCategoryValue.Id> approvals,
AsyncCallback<VoidResult> callback);
+ @Audit
@SignInRequired
void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
AsyncCallback<ReviewerResult> callback);
+ @Audit
@SignInRequired
void removeReviewer(Change.Id id, Account.Id reviewerId,
AsyncCallback<ReviewerResult> callback);
@@ -82,6 +90,7 @@ public interface PatchDetailService extends RemoteJsonService {
/**
* Update the reviewed status for the patch.
*/
+ @Audit
@SignInRequired
void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback);
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
index 075d558d6d..3c5c688f36 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
@@ -20,6 +20,9 @@ import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
public class PatchSetPublishDetail {
@@ -28,6 +31,8 @@ public class PatchSetPublishDetail {
protected Change change;
protected List<PatchLineComment> drafts;
protected List<PermissionRange> labels;
+ protected List<ApprovalDetail> approvals;
+ protected List<SubmitRecord> submitRecords;
protected List<PatchSetApproval> given;
protected boolean canSubmit;
@@ -39,6 +44,23 @@ public class PatchSetPublishDetail {
this.labels = labels;
}
+ public List<ApprovalDetail> getApprovals() {
+ return approvals;
+ }
+
+ public void setApprovals(Collection<ApprovalDetail> list) {
+ approvals = new ArrayList<ApprovalDetail>(list);
+ Collections.sort(approvals, ApprovalDetail.SORT);
+ }
+
+ public void setSubmitRecords(List<SubmitRecord> all) {
+ submitRecords = all;
+ }
+
+ public List<SubmitRecord> getSubmitRecords() {
+ return submitRecords;
+ }
+
public List<PatchSetApproval> getGiven() {
return given;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 20261de496..fd408887cf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -15,11 +15,13 @@
package com.google.gerrit.common.data;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
/** A single permission within an {@link AccessSection} of a project. */
public class Permission implements Comparable<Permission> {
+ public static final String ABANDON = "abandon";
public static final String CREATE = "create";
public static final String FORGE_AUTHOR = "forgeAuthor";
public static final String FORGE_COMMITTER = "forgeCommitter";
@@ -40,6 +42,7 @@ public class Permission implements Comparable<Permission> {
NAMES_LC = new ArrayList<String>();
NAMES_LC.add(OWNER.toLowerCase());
NAMES_LC.add(READ.toLowerCase());
+ NAMES_LC.add(ABANDON.toLowerCase());
NAMES_LC.add(CREATE.toLowerCase());
NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
@@ -73,6 +76,13 @@ public class Permission implements Comparable<Permission> {
return LABEL + labelName;
}
+ public static boolean canBeOnAllProjects(String ref, String permissionName) {
+ if (AccessSection.ALL.equals(ref)) {
+ return !OWNER.equals(permissionName);
+ }
+ return true;
+ }
+
protected String name;
protected boolean exclusiveGroup;
protected List<PermissionRule> rules;
@@ -208,4 +218,23 @@ public class Permission implements Comparable<Permission> {
int index = NAMES_LC.indexOf(a.getName().toLowerCase());
return 0 <= index ? index : NAMES_LC.size();
}
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof Permission)) {
+ return false;
+ }
+
+ final Permission other = (Permission) obj;
+ if (!name.equals(other.name) || exclusiveGroup != other.exclusiveGroup) {
+ return false;
+ }
+ return new HashSet<PermissionRule>(rules)
+ .equals(new HashSet<PermissionRule>(other.rules));
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index 9b6695e1c0..5960165e27 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -257,4 +257,19 @@ public class PermissionRule implements Comparable<PermissionRule> {
}
return Integer.parseInt(value);
}
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof PermissionRule)) {
+ return false;
+ }
+ final PermissionRule other = (PermissionRule)obj;
+ return action.equals(other.action) && force == other.force
+ && min == other.min && max == other.max && group.equals(other.group);
+ }
+
+ @Override
+ public int hashCode() {
+ return group.hashCode();
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index f935c0370e..189384318b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -26,6 +26,7 @@ public class ProjectAccess {
protected List<AccessSection> local;
protected Set<String> ownerOf;
protected boolean isConfigVisible;
+ protected boolean canUpload;
public ProjectAccess() {
}
@@ -94,4 +95,12 @@ public class ProjectAccess {
public void setConfigVisible(boolean isConfigVisible) {
this.isConfigVisible = isConfigVisible;
}
+
+ public boolean canUpload() {
+ return canUpload;
+ }
+
+ public void setCanUpload(boolean canUpload) {
+ this.canUpload = canUpload;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index 1b504b07bc..13c0a48b2b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -14,8 +14,10 @@
package com.google.gerrit.common.data;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -28,14 +30,12 @@ import java.util.Set;
@RpcImpl(version = Version.V2_0)
public interface ProjectAdminService extends RemoteJsonService {
- void visibleProjects(AsyncCallback<ProjectList> callback);
-
void visibleProjectDetails(AsyncCallback<List<ProjectDetail>> callback);
- void suggestParentCandidates(AsyncCallback<List<Project>> callback);
void projectDetail(Project.NameKey projectName,
AsyncCallback<ProjectDetail> callback);
+ @Audit
@SignInRequired
void createNewProject(String projectName, String parentName,
boolean emptyCommit, boolean permissionsOnly,
@@ -44,22 +44,31 @@ public interface ProjectAdminService extends RemoteJsonService {
void projectAccess(Project.NameKey projectName,
AsyncCallback<ProjectAccess> callback);
+ @Audit
@SignInRequired
void changeProjectSettings(Project update,
AsyncCallback<ProjectDetail> callback);
+ @Audit
@SignInRequired
void changeProjectAccess(Project.NameKey projectName, String baseRevision,
String message, List<AccessSection> sections,
AsyncCallback<ProjectAccess> callback);
+ @SignInRequired
+ void reviewProjectAccess(Project.NameKey projectName, String baseRevision,
+ String message, List<AccessSection> sections,
+ AsyncCallback<Change.Id> callback);
+
void listBranches(Project.NameKey projectName,
AsyncCallback<ListBranchesResult> callback);
+ @Audit
@SignInRequired
void addBranch(Project.NameKey projectName, String branchName,
String startingRevision, AsyncCallback<ListBranchesResult> callback);
+ @Audit
@SignInRequired
void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
AsyncCallback<Set<Branch.NameKey>> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java
deleted file mode 100644
index 8511460f65..0000000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2011 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 com.google.gerrit.reviewdb.client.Project;
-
-import java.util.List;
-
-public class ProjectList {
- protected List<Project> projects;
- protected boolean canCreateProject;
-
- public ProjectList() {
- }
-
- public List<Project> getProjects() {
- return projects;
- }
-
- public void setProjects(List<Project> projects) {
- this.projects = projects;
- }
-
- public boolean canCreateProject() {
- return canCreateProject;
- }
-
- public void setCanCreateProject(boolean canCreateProject) {
- this.canCreateProject = canCreateProject;
- }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
index 490378ee44..810e906a3a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
@@ -21,6 +21,9 @@ public abstract class RefConfigSection {
/** Pattern that matches all branches in a project. */
public static final String HEADS = "refs/heads/*";
+ /** Configuration settings for a project {@code refs/meta/config} */
+ public static final String REF_CONFIG = "refs/meta/config";
+
/** Prefix that triggers a regular expression pattern. */
public static final String REGEX_PREFIX = "^";
@@ -45,4 +48,17 @@ public abstract class RefConfigSection {
public void setName(String name) {
this.name = name;
}
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (!(obj instanceof RefConfigSection)) {
+ return false;
+ }
+ return name.equals(((RefConfigSection) obj).name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index 001f9b4710..c0bf81829e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -63,6 +63,9 @@ public class ReviewResult {
/** Review operation invalid because change is closed. */
CHANGE_IS_CLOSED,
+ /** Review operation invalid because change is not abandoned. */
+ CHANGE_NOT_ABANDONED,
+
/** Not permitted to publish this draft patch set */
PUBLISH_NOT_PERMITTED,
@@ -76,7 +79,10 @@ public class ReviewResult {
NOT_A_DRAFT,
/** Error writing change to git repository */
- GIT_ERROR
+ GIT_ERROR,
+
+ /** The destination branch does not exist */
+ DEST_BRANCH_NOT_FOUND
}
protected Type type;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index 5049ba4ad8..365f6a96f0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -68,6 +68,12 @@ public class SubmitRecord {
NEED,
/**
+ * The label may be set, but it's neither necessary for submission
+ * nor does it block submission if set.
+ */
+ MAY,
+
+ /**
* The label is required for submission, but is impossible to complete.
* The likely cause is access has not been granted correctly by the
* project owner or site administrator.
@@ -78,5 +84,34 @@ public class SubmitRecord {
public String label;
public Status status;
public Account.Id appliedBy;
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(label).append(": ").append(status);
+ if (appliedBy != null) {
+ sb.append(" by ").append(appliedBy);
+ }
+ return sb.toString();
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(status);
+ if (status == Status.RULE_ERROR && errorMessage != null) {
+ sb.append('(').append(errorMessage).append(')');
+ }
+ sb.append('[');
+ if (labels != null) {
+ String delimiter = "";
+ for (Label label : labels) {
+ sb.append(delimiter).append(label);
+ delimiter = ", ";
+ }
+ }
+ sb.append(']');
+ return sb.toString();
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 85518b265a..7205b740e1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -26,15 +26,19 @@ import java.util.List;
@RpcImpl(version = Version.V2_0)
public interface SuggestService extends RemoteJsonService {
- void suggestProjectNameKey(String query, int limit,
- AsyncCallback<List<Project.NameKey>> callback);
-
void suggestAccount(String query, Boolean enabled, int limit,
AsyncCallback<List<AccountInfo>> callback);
+ /**
+ * @see #suggestAccountGroup(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
+ */
+ @Deprecated
void suggestAccountGroup(String query, int limit,
AsyncCallback<List<GroupReference>> callback);
+ void suggestAccountGroupForProject(Project.NameKey project, String query,
+ int limit, AsyncCallback<List<GroupReference>> callback);
+
/**
* @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
*/
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 78ccca1104..4a45350db0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -15,7 +15,6 @@
package com.google.gerrit.common.data;
import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
import com.google.gwtjsonrpc.common.RemoteJsonService;
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
deleted file mode 100644
index c25c381c60..0000000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// 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.gerrit.ehcache;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.cache.CacheProvider;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.ProxyCache;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.config.Configuration;
-import net.sf.ehcache.config.DiskStoreConfiguration;
-import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Pool of all declared caches created by {@link CacheModule}s. */
-@Singleton
-public class EhcachePoolImpl implements CachePool {
- private static final Logger log =
- LoggerFactory.getLogger(EhcachePoolImpl.class);
-
- public static class Module extends LifecycleModule {
- @Override
- protected void configure() {
- bind(CachePool.class).to(EhcachePoolImpl.class);
- bind(EhcachePoolImpl.class);
- listener().to(EhcachePoolImpl.Lifecycle.class);
- }
- }
-
- public static class Lifecycle implements LifecycleListener {
- private final EhcachePoolImpl cachePool;
-
- @Inject
- Lifecycle(final EhcachePoolImpl cachePool) {
- this.cachePool = cachePool;
- }
-
- @Override
- public void start() {
- cachePool.start();
- }
-
- @Override
- public void stop() {
- cachePool.stop();
- }
- }
-
- private final Config config;
- private final SitePaths site;
-
- private final Object lock = new Object();
- private final Map<String, CacheProvider<?, ?>> caches;
- private CacheManager manager;
-
- @Inject
- EhcachePoolImpl(@GerritServerConfig final Config cfg, final SitePaths site) {
- this.config = cfg;
- this.site = site;
- this.caches = new HashMap<String, CacheProvider<?, ?>>();
- }
-
- private void start() {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- try {
- System.setProperty("net.sf.ehcache.skipUpdateCheck", "" + true);
- } catch (SecurityException e) {
- // Ignore it, the system is just going to ping some external page
- // using a background thread and there's not much we can do about
- // it now.
- }
-
- manager = new CacheManager(new Factory().toConfiguration());
- for (CacheProvider<?, ?> p : caches.values()) {
- Ehcache eh = manager.getEhcache(p.getName());
- EntryCreator<?, ?> c = p.getEntryCreator();
- if (c != null) {
- p.bind(new PopulatingCache(eh, c));
- } else {
- p.bind(new SimpleCache(eh));
- }
- }
- }
- }
-
- private void stop() {
- synchronized (lock) {
- if (manager != null) {
- manager.shutdown();
- }
- }
- }
-
- /** <i>Discouraged</i> Get the underlying cache descriptions, for statistics. */
- public CacheManager getCacheManager() {
- synchronized (lock) {
- return manager;
- }
- }
-
- public <K, V> ProxyCache<K, V> register(final CacheProvider<K, V> provider) {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- final String n = provider.getName();
- if (caches.containsKey(n) && caches.get(n) != provider) {
- throw new IllegalStateException("Cache \"" + n + "\" already defined");
- }
- caches.put(n, provider);
- return new ProxyCache<K, V>();
- }
- }
-
- private class Factory {
- private static final int MB = 1024 * 1024;
- private final Configuration mgr = new Configuration();
-
- Configuration toConfiguration() {
- configureDiskStore();
- configureDefaultCache();
-
- for (CacheProvider<?, ?> p : caches.values()) {
- final String name = p.getName();
- final CacheConfiguration c = newCache(name);
- c.setMemoryStoreEvictionPolicyFromObject(toPolicy(p.evictionPolicy()));
-
- c.setMaxElementsInMemory(getInt(name, "memorylimit", p.memoryLimit()));
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(getSeconds(name, "maxage", p.maxAge()));
- c.setEternal(c.getTimeToLiveSeconds() == 0);
-
- if (p.disk() && mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(getInt(name, "disklimit", p.diskLimit()));
-
- int v = c.getDiskSpoolBufferSizeMB() * MB;
- v = getInt(name, "diskbuffer", v) / MB;
- c.setDiskSpoolBufferSizeMB(Math.max(1, v));
- c.setOverflowToDisk(c.getMaxElementsOnDisk() > 0);
- c.setDiskPersistent(c.getMaxElementsOnDisk() > 0);
- }
-
- mgr.addCache(c);
- }
-
- return mgr;
- }
-
- private MemoryStoreEvictionPolicy toPolicy(final EvictionPolicy policy) {
- switch (policy) {
- case LFU:
- return MemoryStoreEvictionPolicy.LFU;
-
- case LRU:
- return MemoryStoreEvictionPolicy.LRU;
-
- default:
- throw new IllegalArgumentException("Unsupported " + policy);
- }
- }
-
- private int getInt(String n, String s, int d) {
- return config.getInt("cache", n, s, d);
- }
-
- private long getSeconds(String n, String s, long d) {
- d = MINUTES.convert(d, SECONDS);
- long m = ConfigUtil.getTimeUnit(config, "cache", n, s, d, MINUTES);
- return SECONDS.convert(m, MINUTES);
- }
-
- private void configureDiskStore() {
- boolean needDisk = false;
- for (CacheProvider<?, ?> p : caches.values()) {
- if (p.disk()) {
- needDisk = true;
- break;
- }
- }
- if (!needDisk) {
- return;
- }
-
- File loc = site.resolve(config.getString("cache", null, "directory"));
- if (loc == null) {
- } else if (loc.exists() || loc.mkdirs()) {
- if (loc.canWrite()) {
- final DiskStoreConfiguration c = new DiskStoreConfiguration();
- c.setPath(loc.getAbsolutePath());
- mgr.addDiskStore(c);
- log.info("Enabling disk cache " + loc.getAbsolutePath());
- } else {
- log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
- }
- } else {
- log.warn("Can't create disk cache: " + loc.getAbsolutePath());
- }
- }
-
- private CacheConfiguration newConfiguration() {
- CacheConfiguration c = new CacheConfiguration();
-
- c.setMaxElementsInMemory(1024);
- c.setMemoryStoreEvictionPolicyFromObject(MemoryStoreEvictionPolicy.LFU);
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(0 /* infinite */);
- c.setEternal(true);
-
- if (mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(16384);
- c.setOverflowToDisk(false);
- c.setDiskPersistent(false);
-
- c.setDiskSpoolBufferSizeMB(5);
- c.setDiskExpiryThreadIntervalSeconds(60 * 60);
- }
- return c;
- }
-
- private void configureDefaultCache() {
- mgr.setDefaultCacheConfiguration(newConfiguration());
- }
-
- private CacheConfiguration newCache(final String name) {
- CacheConfiguration c = newConfiguration();
- c.setName(name);
- return c;
- }
- }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
deleted file mode 100644
index f5c6c456f0..0000000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
+++ /dev/null
@@ -1,114 +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.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.constructs.blocking.CacheEntryFactory;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A decorator for {@link Cache} which automatically constructs missing entries.
- * <p>
- * On a cache miss {@link EntryCreator#createEntry(Object)} is invoked, allowing
- * the application specific subclass to compute the entry and return it for
- * caching. During a miss the cache takes a lock related to the missing key,
- * ensuring that at most one thread performs the creation work, and other
- * threads wait for the result. Concurrent creations are possible if two
- * different keys miss and hash to different locks in the internal lock table.
- *
- * @param <K> type of key used to name cache entries.
- * @param <V> type of value stored within a cache entry.
- */
-class PopulatingCache<K, V> implements Cache<K, V> {
- private static final Logger log =
- LoggerFactory.getLogger(PopulatingCache.class);
-
- private final net.sf.ehcache.constructs.blocking.SelfPopulatingCache self;
- private final EntryCreator<K, V> creator;
-
- PopulatingCache(Ehcache s, EntryCreator<K, V> entryCreator) {
- creator = entryCreator;
- final CacheEntryFactory f = new CacheEntryFactory() {
- @SuppressWarnings("unchecked")
- @Override
- public Object createEntry(Object key) throws Exception {
- return creator.createEntry((K) key);
- }
- };
- self = new net.sf.ehcache.constructs.blocking.SelfPopulatingCache(s, f);
- }
-
- /**
- * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
- * <p>
- * The {@link EntryCreator#missing(Object)} method is only invoked if:
- * <ul>
- * <li>{@code key == null}, in which case the application should return a
- * suitable return value that callers can accept, or throw a RuntimeException.
- * <li>{@code createEntry(key)} threw an exception, in which case the entry
- * was not stored in the cache. An entry was recorded in the application log,
- * but a return value is still required.
- * <li>The cache has been shutdown, and access is forbidden.
- * </ul>
- *
- * @param key key to locate.
- * @return either the cached entry, or {@code missing(key)} if not found.
- */
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return creator.missing(key);
- }
-
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- }
- return m != null ? (V) m.getObjectValue() : creator.missing(key);
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- /** Remove all cached items, forcing them to be created again on demand. */
- public void removeAll() {
- self.removeAll();
- }
-
- public void put(K key, V value) {
- self.put(new Element(key, value));
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
deleted file mode 100644
index e4428e3ed1..0000000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// 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.gerrit.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-final class SimpleCache<K, V> implements Cache<K, V> {
- private static final Logger log = LoggerFactory.getLogger(SimpleCache.class);
-
- private final Ehcache self;
-
- SimpleCache(final Ehcache self) {
- this.self = self;
- }
-
- Ehcache getEhcache() {
- return self;
- }
-
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return null;
- }
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- }
- return m != null ? (V) m.getObjectValue() : null;
- }
-
- public void put(final K key, final V value) {
- self.put(new Element(key, value));
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- public void removeAll() {
- self.removeAll();
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
diff --git a/gerrit-extension-api/.gitignore b/gerrit-extension-api/.gitignore
new file mode 100644
index 0000000000..4e1ec9c6c1
--- /dev/null
+++ b/gerrit-extension-api/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-extension-api.iml
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..f9fe34593f
--- /dev/null
+++ b/gerrit-extension-api/.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-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
index 82eb859e3b..8667cfd4a3 100644
--- a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
@@ -1,3 +1,3 @@
#Tue Sep 02 16:59:24 PDT 2008
eclipse.preferences.version=1
-encoding/<project>=UTF-8
+line.separator=\n
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000000..470942d4f6
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+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_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_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_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.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.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.format_guardian_clause_on_one_line=false
+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_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=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_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_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_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_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_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_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.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_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000000..d4218a5fc0
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
new file mode 100644
index 0000000000..ff672d5cc8
--- /dev/null
+++ b/gerrit-extension-api/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.5-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>gerrit-extension-api</artifactId>
+ <name>Gerrit Code Review - Extension API</name>
+
+ <description>
+ Interfaces describing the extension API
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.inject.extensions</groupId>
+ <artifactId>guice-servlet</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <createSourcesJar>true</createSourcesJar>
+ <shadedArtifactAttached>true</shadedArtifactAttached>
+ <shadedClassifierName>all</shadedClassifierName>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
new file mode 100644
index 0000000000..4811e40781
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
@@ -0,0 +1,52 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to auto-registered, exported types.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class they want exported for access.
+ * <p>
+ * For SSH commands the @Export annotation names the subcommand:
+ *
+ * <pre>
+ * @Export("print")
+ * class MyCommand extends SshCommand {
+ * </pre>
+ *
+ * For HTTP servlets, the @Export annotation names the URL the servlet is bound
+ * to, relative to the plugin or extension's namespace within the Gerrit
+ * container.
+ *
+ * <pre>
+ * @Export("/index.html")
+ * class ShowIndexHtml extends HttpServlet {
+ * </pre>
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Export {
+ String value();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
new file mode 100644
index 0000000000..a3e72bccc3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
@@ -0,0 +1,52 @@
+// 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.extensions.annotations;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+
+final class ExportImpl implements Export, Serializable {
+ private static final long serialVersionUID = 0;
+ private final String value;
+
+ ExportImpl(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Export.class;
+ }
+
+ @Override
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return (127 * "value".hashCode()) ^ value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Export && value.equals(((Export) o).value());
+ }
+
+ @Override
+ public String toString() {
+ return "@" + Export.class.getName() + "(value=" + value + ")";
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
new file mode 100644
index 0000000000..c48bcfb9c0
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -0,0 +1,26 @@
+// 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.extensions.annotations;
+
+/** Static constructors for {@link Export} annotations. */
+public final class Exports {
+ /** Create an annotation to export under a specific name. */
+ public static Export named(String name) {
+ return new ExportImpl(name);
+ }
+
+ private Exports() {
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
new file mode 100644
index 0000000000..4799f5e1ff
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
@@ -0,0 +1,41 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for interfaces that accept auto-registered implementations.
+ * <p>
+ * Interfaces that accept automatically registered implementations into their
+ * {@link DynamicSet} must be tagged with this annotation.
+ * <p>
+ * Plugins or extensions that implement an {@code @ExtensionPoint} interface
+ * should use the {@link Listen} annotation to automatically register.
+ *
+ * @see Listen
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ExtensionPoint {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
new file mode 100644
index 0000000000..e4ba9316c4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
@@ -0,0 +1,39 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for auto-registered extension point implementations.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class that implements an unnamed extension point, such as a
+ * notification listener. Gerrit will automatically determine which extension
+ * points to apply based on the interfaces the type implements.
+ *
+ * @see Export
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Listen {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
new file mode 100644
index 0000000000..bf2b09a543
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
@@ -0,0 +1,42 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Local path where a plugin can store its own private data.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * a directory where it can store configuration or other data that is private:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginData java.io.File myDir) {
+ * new FileInputStream(new File(myDir, &quot;my.config&quot;));
+ * }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginData {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
new file mode 100644
index 0000000000..672bab2fec
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
@@ -0,0 +1,42 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to a String containing the plugin or extension name.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * the name that an administrator has installed the plugin or extension under:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginName String myName) {
+ * ...
+ * }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginName {
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index adaf6466fa..382f4eafb9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd;
+package com.google.gerrit.extensions.annotations;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -21,12 +21,11 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
- * Annotation tagged on a concrete Command that requires administrator access.
- * <p>
- * Currently this annotation is only enforced by DispatchCommand after it has
- * created the command object, but before it populates it or starts execution.
+ * Annotation on {@link SshCommand} or {@link RestApiServlet} declaring a
+ * capability must be granted.
*/
-@Target( {ElementType.TYPE})
+@Target({ElementType.TYPE})
@Retention(RUNTIME)
-public @interface AdminCommand {
+public @interface RequiresCapability {
+ String value();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
new file mode 100644
index 0000000000..438500d5e6
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -0,0 +1,34 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.List;
+
+/** Notified when one or more references are modified. */
+@ExtensionPoint
+public interface GitReferenceUpdatedListener {
+ public interface Update {
+ String getRefName();
+ }
+
+ public interface Event {
+ String getProjectName();
+ List<Update> getUpdates();
+ }
+
+ void onGitReferenceUpdated(Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index e6b06ef5df..93da347172 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.lifecycle;
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
import java.util.EventListener;
/** Listener interested in server startup and shutdown events. */
+@ExtensionPoint
public interface LifecycleListener extends EventListener {
/** Invoke when the server is starting. */
public void start();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
new file mode 100644
index 0000000000..7eed7d4724
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
@@ -0,0 +1,29 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+
+/** Notified whenever a project is created on the master. */
+@ExtensionPoint
+public interface NewProjectCreatedListener {
+ public interface Event {
+ String getProjectName();
+ String getHeadName();
+ }
+
+ void onNewProjectCreated(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
new file mode 100644
index 0000000000..40bbb80f11
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -0,0 +1,163 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Types;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A map of members that can be modified as plugins reload.
+ * <p>
+ * Maps index their members by plugin name and export name.
+ * <p>
+ * DynamicMaps are always mapped as singletons in Guice. Maps store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+public abstract class DynamicMap<T> {
+ /**
+ * Declare a singleton {@code DynamicMap<T>} with a binder.
+ * <p>
+ * Maps must be defined in a Guice module before they can be bound:
+ *
+ * <pre>
+ * DynamicMap.mapOf(binder(), Interface.class);
+ * bind(Interface.class)
+ * .annotatedWith(Exports.named(&quot;foo&quot;))
+ * .to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of value in the map.
+ */
+ public static <T> void mapOf(Binder binder, Class<T> member) {
+ mapOf(binder, TypeLiteral.get(member));
+ }
+
+ /**
+ * Declare a singleton {@code DynamicMap<T>} with a binder.
+ * <p>
+ * Maps must be defined in a Guice module before they can be bound:
+ *
+ * <pre>
+ * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
+ * bind(new TypeLiteral<Thing<Bar>>() {})
+ * .annotatedWith(Exports.named(&quot;foo&quot;))
+ * .to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of value in the map.
+ */
+ public static <T> void mapOf(Binder binder, TypeLiteral<T> member) {
+ @SuppressWarnings("unchecked")
+ Key<DynamicMap<T>> key = (Key<DynamicMap<T>>) Key.get(
+ Types.newParameterizedType(DynamicMap.class, member.getType()));
+ binder.bind(key)
+ .toProvider(new DynamicMapProvider<T>(member))
+ .in(Scopes.SINGLETON);
+ }
+
+ final ConcurrentMap<NamePair, Provider<T>> items;
+
+ DynamicMap() {
+ items = new ConcurrentHashMap<NamePair, Provider<T>>(
+ 16 /* initial size */,
+ 0.75f /* load factor */,
+ 1 /* concurrency level of 1, load/unload is single threaded */);
+ }
+
+ /**
+ * Lookup an implementation by name.
+ *
+ * @param pluginName local name of the plugin providing the item.
+ * @param exportName name the plugin exports the item as.
+ * @return the implementation. Null if the plugin is not running, or if the
+ * plugin does not export this name.
+ * @throws ProvisionException if the registered provider is unable to obtain
+ * an instance of the requested implementation.
+ */
+ public T get(String pluginName, String exportName) {
+ Provider<T> p = items.get(new NamePair(pluginName, exportName));
+ return p != null ? p.get() : null;
+ }
+
+ /**
+ * Get the names of all running plugins supplying this type.
+ *
+ * @return sorted set of active plugins that supply at least one item.
+ */
+ public SortedSet<String> plugins() {
+ SortedSet<String> r = new TreeSet<String>();
+ for (NamePair p : items.keySet()) {
+ r.add(p.pluginName);
+ }
+ return Collections.unmodifiableSortedSet(r);
+ }
+
+ /**
+ * Get the items exported by a single plugin.
+ *
+ * @param pluginName name of the plugin.
+ * @return items exported by a plugin, keyed by the export name.
+ */
+ public SortedMap<String, Provider<T>> byPlugin(String pluginName) {
+ SortedMap<String, Provider<T>> r = new TreeMap<String, Provider<T>>();
+ for (Map.Entry<NamePair, Provider<T>> e : items.entrySet()) {
+ if (e.getKey().pluginName.equals(pluginName)) {
+ r.put(e.getKey().exportName, e.getValue());
+ }
+ }
+ return Collections.unmodifiableSortedMap(r);
+ }
+
+ static class NamePair {
+ private final String pluginName;
+ private final String exportName;
+
+ NamePair(String pn, String en) {
+ this.pluginName = pn;
+ this.exportName = en;
+ }
+
+ @Override
+ public int hashCode() {
+ return pluginName.hashCode() * 31 + exportName.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof NamePair) {
+ NamePair np = (NamePair) other;
+ return pluginName.equals(np.pluginName)
+ && exportName.equals(np.exportName);
+ }
+ return false;
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
new file mode 100644
index 0000000000..c6e4701537
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -0,0 +1,48 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+
+class DynamicMapProvider<T> implements Provider<DynamicMap<T>> {
+ private final TypeLiteral<T> type;
+
+ @Inject
+ private Injector injector;
+
+ DynamicMapProvider(TypeLiteral<T> type) {
+ this.type = type;
+ }
+
+ public DynamicMap<T> get() {
+ PrivateInternals_DynamicMapImpl<T> m =
+ new PrivateInternals_DynamicMapImpl<T>();
+ List<Binding<T>> bindings = injector.findBindingsByType(type);
+ if (bindings != null) {
+ for (Binding<T> b : bindings) {
+ if (b.getKey().getAnnotation() != null) {
+ m.put("gerrit", b.getKey(), b.getProvider());
+ }
+ }
+ }
+ return m;
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
new file mode 100644
index 0000000000..ec34887083
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -0,0 +1,253 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.name.Named;
+import com.google.inject.util.Providers;
+import com.google.inject.util.Types;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A set of members that can be modified as plugins reload.
+ * <p>
+ * DynamicSets are always mapped as singletons in Guice. Sets store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+public class DynamicSet<T> implements Iterable<T> {
+ /**
+ * Declare a singleton {@code DynamicSet<T>} with a binder.
+ * <p>
+ * Sets must be defined in a Guice module before they can be bound:
+ * <pre>
+ * DynamicSet.setOf(binder(), Interface.class);
+ * DynamicSet.bind(binder(), Interface.class).to(Impl.class);
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of entry in the set.
+ */
+ public static <T> void setOf(Binder binder, Class<T> member) {
+ setOf(binder, TypeLiteral.get(member));
+ }
+
+ /**
+ * Declare a singleton {@code DynamicSet<T>} with a binder.
+ * <p>
+ * Sets must be defined in a Guice module before they can be bound:
+ * <pre>
+ * DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+ * </pre>
+ *
+ * @param binder a new binder created in the module.
+ * @param member type of entry in the set.
+ */
+ public static <T> void setOf(Binder binder, TypeLiteral<T> member) {
+ @SuppressWarnings("unchecked")
+ Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
+ Types.newParameterizedType(DynamicSet.class, member.getType()));
+ binder.bind(key)
+ .toProvider(new DynamicSetProvider<T>(member))
+ .in(Scopes.SINGLETON);
+ }
+
+ /**
+ * Bind one implementation into the set using a unique annotation.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+ return bind(binder, TypeLiteral.get(type));
+ }
+
+ /**
+ * Bind one implementation into the set using a unique annotation.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+ return binder.bind(type).annotatedWith(UniqueAnnotations.create());
+ }
+
+ /**
+ * Bind a named implementation into the set.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @param name {@code @Named} annotation to apply instead of a unique
+ * annotation.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+ Class<T> type,
+ Named name) {
+ return bind(binder, TypeLiteral.get(type));
+ }
+
+ /**
+ * Bind a named implementation into the set.
+ *
+ * @param binder a new binder created in the module.
+ * @param type type of entries in the set.
+ * @param name {@code @Named} annotation to apply instead of a unique
+ * annotation.
+ * @return a binder to continue configuring the new set member.
+ */
+ public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+ TypeLiteral<T> type,
+ Named name) {
+ return binder.bind(type).annotatedWith(name);
+ }
+
+ private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
+
+ DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
+ items = new CopyOnWriteArrayList<AtomicReference<Provider<T>>>(base);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
+ return new Iterator<T>() {
+ private T next;
+
+ @Override
+ public boolean hasNext() {
+ while (next == null && itr.hasNext()) {
+ Provider<T> p = itr.next().get();
+ if (p != null) {
+ try {
+ next = p.get();
+ } catch (RuntimeException e) {
+ // TODO Log failed member of DynamicSet.
+ }
+ }
+ }
+ return next != null;
+ }
+
+ @Override
+ public T next() {
+ if (hasNext()) {
+ T result = next;
+ next = null;
+ return result;
+ }
+ throw new NoSuchElementException();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ /**
+ * Add one new element to the set.
+ *
+ * @param item the item to add to the collection. Must not be null.
+ * @return handle to remove the item at a later point in time.
+ */
+ public RegistrationHandle add(final T item) {
+ return add(Providers.of(item));
+ }
+
+ /**
+ * Add one new element to the set.
+ *
+ * @param item the item to add to the collection. Must not be null.
+ * @return handle to remove the item at a later point in time.
+ */
+ public RegistrationHandle add(final Provider<T> item) {
+ final AtomicReference<Provider<T>> ref =
+ new AtomicReference<Provider<T>>(item);
+ items.add(ref);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ if (ref.compareAndSet(item, null)) {
+ items.remove(ref);
+ }
+ }
+ };
+ }
+
+ /**
+ * Add one new element that may be hot-replaceable in the future.
+ *
+ * @param key unique description from the item's Guice binding. This can be
+ * later obtained from the registration handle to facilitate matching
+ * with the new equivalent instance during a hot reload.
+ * @param item the item to add to the collection right now. Must not be null.
+ * @return a handle that can remove this item later, or hot-swap the item
+ * without it ever leaving the collection.
+ */
+ public ReloadableRegistrationHandle<T> add(Key<T> key, Provider<T> item) {
+ AtomicReference<Provider<T>> ref = new AtomicReference<Provider<T>>(item);
+ items.add(ref);
+ return new ReloadableHandle(ref, key, item);
+ }
+
+ private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+ private final AtomicReference<Provider<T>> ref;
+ private final Key<T> key;
+ private final Provider<T> item;
+
+ ReloadableHandle(AtomicReference<Provider<T>> ref,
+ Key<T> key,
+ Provider<T> item) {
+ this.ref = ref;
+ this.key = key;
+ this.item = item;
+ }
+
+ @Override
+ public void remove() {
+ if (ref.compareAndSet(item, null)) {
+ items.remove(ref);
+ }
+ }
+
+ @Override
+ public Key<T> getKey() {
+ return key;
+ }
+
+ @Override
+ public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
+ if (ref.compareAndSet(item, newItem)) {
+ return new ReloadableHandle(ref, newKey, newItem);
+ }
+ return null;
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
new file mode 100644
index 0000000000..6c21553f6d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -0,0 +1,65 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
+ private static final Class<?> UNIQUE_ANNOTATION =
+ UniqueAnnotations.create().getClass();
+ private final TypeLiteral<T> type;
+
+ @Inject
+ private Injector injector;
+
+ DynamicSetProvider(TypeLiteral<T> type) {
+ this.type = type;
+ }
+
+ public DynamicSet<T> get() {
+ return new DynamicSet<T>(find(injector, type));
+ }
+
+ private static <T> List<AtomicReference<Provider<T>>> find(
+ Injector src,
+ TypeLiteral<T> type) {
+ List<Binding<T>> bindings = src.findBindingsByType(type);
+ int cnt = bindings != null ? bindings.size() : 0;
+ if (cnt == 0) {
+ return Collections.emptyList();
+ }
+ List<AtomicReference<Provider<T>>> r = newList(cnt);
+ for (Binding<T> b : bindings) {
+ if (b.getKey().getAnnotation() != null) {
+ r.add(new AtomicReference<Provider<T>>(b.getProvider()));
+ }
+ }
+ return r;
+ }
+
+ private static <T> List<AtomicReference<Provider<T>>> newList(int cnt) {
+ return new ArrayList<AtomicReference<Provider<T>>>(cnt);
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
new file mode 100644
index 0000000000..355879436e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -0,0 +1,97 @@
+// 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.extensions.registration;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
+ PrivateInternals_DynamicMapImpl() {
+ }
+
+ /**
+ * Store one new element into the map.
+ *
+ * @param pluginName unique name of the plugin providing the export.
+ * @param exportName name the plugin has exported the item as.
+ * @param item the item to add to the collection. Must not be null.
+ * @return handle to remove the item at a later point in time.
+ */
+ public RegistrationHandle put(
+ String pluginName, String exportName,
+ final Provider<T> item) {
+ final NamePair key = new NamePair(pluginName, exportName);
+ items.put(key, item);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ items.remove(key, item);
+ }
+ };
+ }
+
+ /**
+ * Store one new element that may be hot-replaceable in the future.
+ *
+ * @param pluginName unique name of the plugin providing the export.
+ * @param key unique description from the item's Guice binding. This can be
+ * later obtained from the registration handle to facilitate matching
+ * with the new equivalent instance during a hot reload. The key must
+ * use an {@link @Export} annotation.
+ * @param item the item to add to the collection right now. Must not be null.
+ * @return a handle that can remove this item later, or hot-swap the item
+ * without it ever leaving the collection.
+ */
+ public ReloadableRegistrationHandle<T> put(
+ String pluginName, Key<T> key,
+ Provider<T> item) {
+ String exportName = ((Export) key.getAnnotation()).value();
+ NamePair np = new NamePair(pluginName, exportName);
+ items.put(np, item);
+ return new ReloadableHandle(np, key, item);
+ }
+
+ private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+ private final NamePair np;
+ private final Key<T> key;
+ private final Provider<T> item;
+
+ ReloadableHandle(NamePair np, Key<T> key, Provider<T> item) {
+ this.np = np;
+ this.key = key;
+ this.item = item;
+ }
+
+ @Override
+ public void remove() {
+ items.remove(np, item);
+ }
+
+ @Override
+ public Key<T> getKey() {
+ return key;
+ }
+
+ @Override
+ public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
+ if (items.replace(np, item, newItem)) {
+ return new ReloadableHandle(np, newKey, newItem);
+ }
+ return null;
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
new file mode 100644
index 0000000000..66dd45d657
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -0,0 +1,175 @@
+// 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.extensions.registration;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicTypes {
+ public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicSet.class) {
+ ParameterizedType p = (ParameterizedType) type.getType();
+ m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+ (DynamicSet<?>) e.getValue().getProvider().get());
+ }
+ }
+ if (m.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(m);
+ }
+
+ public static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicMap<?>> m = newHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicMap.class) {
+ ParameterizedType p = (ParameterizedType) type.getType();
+ m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+ (DynamicMap<?>) e.getValue().getProvider().get());
+ }
+ }
+ if (m.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(m);
+ }
+
+ public static List<RegistrationHandle> attachSets(
+ Injector src,
+ Map<TypeLiteral<?>, DynamicSet<?>> sets) {
+ if (src == null || sets == null || sets.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+ try {
+ for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+ for (Binding<Object> b : bindings(src, type)) {
+ if (b.getKey().getAnnotation() != null) {
+ handles.add(set.add(b.getKey(), b.getProvider()));
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ remove(handles);
+ throw e;
+ } catch (Error e) {
+ remove(handles);
+ throw e;
+ }
+ return handles;
+ }
+
+ public static List<RegistrationHandle> attachMaps(
+ Injector src,
+ String groupName,
+ Map<TypeLiteral<?>, DynamicMap<?>> maps) {
+ if (src == null || maps == null || maps.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+ try {
+ for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ PrivateInternals_DynamicMapImpl<Object> set =
+ (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+ for (Binding<Object> b : bindings(src, type)) {
+ if (b.getKey().getAnnotation() != null) {
+ handles.add(set.put(groupName, b.getKey(), b.getProvider()));
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ remove(handles);
+ throw e;
+ } catch (Error e) {
+ remove(handles);
+ throw e;
+ }
+ return handles;
+ }
+
+ public static LifecycleListener registerInParentInjectors() {
+ return new LifecycleListener() {
+ private List<RegistrationHandle> handles;
+
+ @Inject
+ private Injector self;
+
+ @Override
+ public void start() {
+ handles = new ArrayList<RegistrationHandle>(4);
+ Injector parent = self.getParent();
+ while (parent != null) {
+ handles.addAll(attachSets(self, dynamicSetsOf(parent)));
+ handles.addAll(attachMaps(self, "gerrit", dynamicMapsOf(parent)));
+ parent = parent.getParent();
+ }
+ if (handles.isEmpty()) {
+ handles = null;
+ }
+ }
+
+ @Override
+ public void stop() {
+ remove(handles);
+ handles = null;
+ }
+ };
+ }
+
+ private static void remove(List<RegistrationHandle> handles) {
+ if (handles != null) {
+ for (RegistrationHandle handle : handles) {
+ handle.remove();
+ }
+ }
+ }
+
+ private static <K,V> Map<K, V> newHashMap() {
+ return new HashMap<K,V>();
+ }
+
+ private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+ return src.findBindingsByType(type);
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
new file mode 100644
index 0000000000..2243786249
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
@@ -0,0 +1,21 @@
+// 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.extensions.registration;
+
+/** Handle for registered information. */
+public interface RegistrationHandle {
+ /** Delete this registration. */
+ public void remove();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
new file mode 100644
index 0000000000..7284296550
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
@@ -0,0 +1,24 @@
+// 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.extensions.registration;
+
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
+ public Key<T> getKey();
+
+ public RegistrationHandle replace(Key<T> key, Provider<T> item);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
new file mode 100644
index 0000000000..3d2df21226
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
@@ -0,0 +1,42 @@
+// 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.extensions.systemstatus;
+
+/** Exports current server information to an extension. */
+public interface ServerInformation {
+ /** Current state of the server. */
+ public enum State {
+ /**
+ * The server is starting up, and network connections are not yet being
+ * accepted. Plugins or extensions starting during this time are starting
+ * for the first time in this process.
+ */
+ STARTUP,
+
+ /**
+ * The server is running and handling requests. Plugins starting during this
+ * state may be reloading, or being installed into a running system.
+ */
+ RUNNING,
+
+ /**
+ * The server is attempting a graceful halt of operations and will exit (or
+ * be killed by the operating system) soon.
+ */
+ SHUTDOWN;
+ }
+
+ State getState();
+}
diff --git a/gerrit-gwtdebug/.gitignore b/gerrit-gwtdebug/.gitignore
index 194bedcbc4..4207862485 100644
--- a/gerrit-gwtdebug/.gitignore
+++ b/gerrit-gwtdebug/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtdebug.iml \ No newline at end of file
diff --git a/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
index 36e1448ea5..e9441bb123 100644
--- a/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:38 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 734f645b33..01b93a6f70 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/.gitignore b/gerrit-gwtui/.gitignore
index 194bedcbc4..53d46b3c77 100644
--- a/gerrit-gwtui/.gitignore
+++ b/gerrit-gwtui/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtui.iml \ No newline at end of file
diff --git a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
index c780f4418c..e9441bb123 100644
--- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index f14daf64e5..b3291d16a6 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtui</artifactId>
@@ -168,12 +168,33 @@ limitations under the License.
</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>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 3aee0e27ca..aefad27ddf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -14,9 +14,11 @@
package com.google.gerrit.client;
+import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_GROUP;
import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_PROJECT;
import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
+import static com.google.gerrit.common.PageLinks.ADMIN_PLUGINS;
import static com.google.gerrit.common.PageLinks.MINE;
import static com.google.gerrit.common.PageLinks.REGISTER;
import static com.google.gerrit.common.PageLinks.SETTINGS;
@@ -46,8 +48,10 @@ import com.google.gerrit.client.account.ValidateEmailScreen;
import com.google.gerrit.client.admin.AccountGroupInfoScreen;
import com.google.gerrit.client.admin.AccountGroupMembersScreen;
import com.google.gerrit.client.admin.AccountGroupScreen;
+import com.google.gerrit.client.admin.CreateGroupScreen;
import com.google.gerrit.client.admin.CreateProjectScreen;
import com.google.gerrit.client.admin.GroupListScreen;
+import com.google.gerrit.client.admin.PluginListScreen;
import com.google.gerrit.client.admin.ProjectAccessScreen;
import com.google.gerrit.client.admin.ProjectBranchesScreen;
import com.google.gerrit.client.admin.ProjectInfoScreen;
@@ -58,6 +62,7 @@ import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.client.changes.CustomDashboardScreen;
import com.google.gerrit.client.changes.PatchTable;
import com.google.gerrit.client.changes.PublishCommentScreen;
import com.google.gerrit.client.changes.QueryScreen;
@@ -234,6 +239,10 @@ public class Dispatcher {
}
if (matchExact("mine,drafts", token)) {
+ return PageLinks.toChangeQuery("is:draft");
+ }
+
+ if (matchExact("mine,comments", token)) {
return PageLinks.toChangeQuery("has:draft");
}
@@ -361,8 +370,18 @@ public class Dispatcher {
}
private static void dashboard(final String token) {
- Gerrit.display(token, //
- new AccountDashboardScreen(Account.Id.parse(skip(token))));
+ String rest = skip(token);
+ if (rest.matches("[0-9]+")) {
+ Gerrit.display(token, new AccountDashboardScreen(Account.Id.parse(rest)));
+ return;
+ }
+
+ if (rest.startsWith("?")) {
+ Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
+ return;
+ }
+
+ Gerrit.display(token, new NotFoundScreen());
}
private static void change(final String token) {
@@ -606,10 +625,18 @@ public class Dispatcher {
} else if (matchPrefix("/admin/projects/", token)) {
Gerrit.display(token, selectProject());
+ } else if (matchPrefix(ADMIN_PLUGINS, token)
+ || matchExact("/admin/plugins", token)) {
+ Gerrit.display(token, new PluginListScreen());
+
} else if (matchExact(ADMIN_CREATE_PROJECT, token)
|| matchExact("/admin/create-project", token)) {
Gerrit.display(token, new CreateProjectScreen());
+ } else if (matchExact(ADMIN_CREATE_GROUP, token)
+ || matchExact("/admin/create-group", token)) {
+ Gerrit.display(token, new CreateGroupScreen());
+
} else {
Gerrit.display(token, new NotFoundScreen());
}
@@ -695,8 +722,7 @@ public class Dispatcher {
return new ProjectInfoScreen(k);
}
- if (ProjectScreen.BRANCH.equals(panel)
- && !k.equals(Gerrit.getConfig().getWildProject())) {
+ if (ProjectScreen.BRANCH.equals(panel)) {
return new ProjectBranchesScreen(k);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 74a2678555..13bba1237d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -25,6 +25,7 @@ import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -94,6 +95,11 @@ public class ErrorDialog extends PluginSafePopupPanel {
body.add(message.toBlockWidget());
}
+ public ErrorDialog(final Widget w) {
+ this();
+ body.add(w);
+ }
+
/** Create a dialog box to nicely format an exception. */
public ErrorDialog(final Throwable what) {
this();
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 e578eae473..f10762a3db 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
@@ -25,9 +25,10 @@ import java.util.Date;
public class FormatUtil {
private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
- private static DateTimeFormat sTime = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.TIME_SHORT);
- private static DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d");
- private static DateTimeFormat mDate = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_MEDIUM);
+ private static DateTimeFormat sTime;
+ private static DateTimeFormat sDate;
+ private static DateTimeFormat sdtFmt;
+ private static DateTimeFormat mDate;
private static DateTimeFormat dtfmt;
public static void setPreferences(AccountGeneralPreferences pref) {
@@ -41,10 +42,12 @@ public class FormatUtil {
}
String fmt_sTime = pref.getTimeFormat().getFormat();
+ String fmt_sDate = pref.getDateFormat().getShortFormat();
String fmt_mDate = pref.getDateFormat().getLongFormat();
sTime = DateTimeFormat.getFormat(fmt_sTime);
- sDate = DateTimeFormat.getFormat(pref.getDateFormat().getShortFormat());
+ sDate = DateTimeFormat.getFormat(fmt_sDate);
+ sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
mDate = DateTimeFormat.getFormat(fmt_mDate);
dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
}
@@ -75,6 +78,32 @@ public class FormatUtil {
}
}
+ /** Format a date using a really short format. */
+ public static String shortFormatDayTime(Date dt) {
+ if (dt == null) {
+ return "";
+ }
+
+ ensureInited();
+ final Date now = new Date();
+ dt = new Date(dt.getTime());
+ if (mDate.format(now).equals(mDate.format(dt))) {
+ // Same day as today, report only the time.
+ //
+ return sTime.format(dt);
+
+ } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
+ // Within the last year, show a shorter date.
+ //
+ return sdtFmt.format(dt);
+
+ } else {
+ // Report only date and year, its far away from now.
+ //
+ return mDate.format(dt);
+ }
+ }
+
/** Format a date using the locale's medium length format. */
public static String mediumFormat(final Date dt) {
if (dt == 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 6dbfeeefa8..8fe658ba59 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
@@ -14,7 +14,13 @@
package com.google.gerrit.client;
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
+import com.google.gerrit.client.account.AccountCapabilities;
import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
+import com.google.gerrit.client.auth.openid.OpenIdSsoPanel;
import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
import com.google.gerrit.client.changes.ChangeConstants;
import com.google.gerrit.client.changes.ChangeListScreen;
@@ -53,8 +59,8 @@ import com.google.gwt.user.client.ui.Accessibility;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.InlineHTML;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
@@ -255,6 +261,13 @@ public class Gerrit implements EntryPoint {
Location.assign(selfRedirect("/become"));
break;
+ case OPENID_SSO:
+ final RootPanel gBody = RootPanel.get("gerrit_body");
+ OpenIdSsoPanel singleSignOnPanel = new OpenIdSsoPanel();
+ gBody.add(singleSignOnPanel);
+ singleSignOnPanel.authenticate(SignInMode.SIGN_IN, token);
+ break;
+
case OPENID:
new OpenIdSignInDialog(SignInMode.SIGN_IN, token, null).center();
break;
@@ -266,7 +279,7 @@ public class Gerrit implements EntryPoint {
}
}
- private static String loginRedirect(String token) {
+ public static String loginRedirect(String token) {
if (token == null) {
token = "";
} else if (token.startsWith("/")) {
@@ -275,7 +288,7 @@ public class Gerrit implements EntryPoint {
return selfRedirect("/login/" + token);
}
- private static String selfRedirect(String suffix) {
+ public static String selfRedirect(String suffix) {
// Clean up the path. Users seem to like putting extra slashes into the URL
// which can break redirections by misinterpreting at either client or server.
String path = Location.getPath();
@@ -323,6 +336,7 @@ public class Gerrit implements EntryPoint {
Cookies.removeCookie("GerritAccount");
}
+ @Override
public void onModuleLoad() {
UserAgent.assertNotInIFrame();
@@ -332,6 +346,7 @@ public class Gerrit implements EntryPoint {
e = URL.encodeQueryString(e);
e = fixPathImpl(e);
e = fixColonImpl(e);
+ e = fixDoubleQuote(e);
return e;
}
@@ -345,6 +360,9 @@ public class Gerrit implements EntryPoint {
private native String fixColonImpl(String path)
/*-{ return path.replace(/%3A/g, ":"); }-*/;
+
+ private native String fixDoubleQuote(String path)
+ /*-{ return path.replace(/%22/g, '"'); }-*/;
});
initHostname();
@@ -430,9 +448,19 @@ public class Gerrit implements EntryPoint {
vs = "dev";
}
- final HTML version = new HTML(M.poweredBy(vs));
- version.setStyleName(RESOURCES.css().version());
- btmmenu.add(version);
+ FlowPanel poweredBy = new FlowPanel();
+ poweredBy.setStyleName(RESOURCES.css().version());
+ poweredBy.add(new InlineHTML(M.poweredBy(vs)));
+ if (getConfig().getReportBugUrl() != null) {
+ poweredBy.add(new InlineLabel(" | "));
+ Anchor a = new Anchor(
+ C.reportBug(),
+ getConfig().getReportBugUrl());
+ a.setTarget("_blank");
+ a.setStyleName("");
+ poweredBy.add(a);
+ }
+ btmmenu.add(poweredBy);
}
private void onModuleLoad2() {
@@ -548,9 +576,10 @@ public class Gerrit implements EntryPoint {
if (signedIn) {
m = new LinkMenuBar();
addLink(m, C.menuMyChanges(), PageLinks.MINE);
- addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("has:draft"));
+ addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open"));
addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred"));
+ addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
menuLeft.add(m, C.menuMine());
menuLeft.selectTab(1);
} else {
@@ -567,11 +596,44 @@ public class Gerrit implements EntryPoint {
addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
+ final LinkMenuBar projectsBar = new LinkMenuBar();
+ addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
+ if(signedIn) {
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities result) {
+ if (result.canPerform(CREATE_PROJECT)) {
+ addLink(projectsBar, C.menuProjectsCreate(), PageLinks.ADMIN_CREATE_PROJECT);
+ }
+ }
+ }, CREATE_PROJECT);
+ }
+ menuLeft.add(projectsBar, C.menuProjects());
+
if (signedIn) {
- m = new LinkMenuBar();
- addLink(m, C.menuGroups(), PageLinks.ADMIN_GROUPS);
- addLink(m, C.menuProjects(), PageLinks.ADMIN_PROJECTS);
- menuLeft.add(m, C.menuAdmin());
+ final LinkMenuBar groupsBar = new LinkMenuBar();
+ addLink(groupsBar, C.menuGroupsList(), PageLinks.ADMIN_GROUPS);
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities result) {
+ if (result.canPerform(CREATE_GROUP)) {
+ addLink(groupsBar, C.menuGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
+ }
+ }
+ }, CREATE_GROUP);
+ menuLeft.add(groupsBar, C.menuGroups());
+
+ final LinkMenuBar pluginsBar = new LinkMenuBar();
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities result) {
+ if (result.canPerform(ADMINISTRATE_SERVER)) {
+ addLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS);
+ menuLeft.insert(pluginsBar, C.menuPlugins(),
+ menuLeft.getWidgetIndex(groupsBar) + 1);
+ }
+ }
+ }, ADMINISTRATE_SERVER);
}
if (getConfig().isDocumentationAvailable()) {
@@ -610,14 +672,25 @@ public class Gerrit implements EntryPoint {
});
break;
+ case OPENID_SSO:
+ menuRight.addItem(C.menuSignIn(), new Command() {
+ public void execute() {
+ doSignIn(History.getToken());
+ }
+ });
+ break;
+
case LDAP:
case LDAP_BIND:
case CUSTOM_EXTENSION:
if (cfg.getRegisterUrl() != null) {
menuRight.add(anchor(C.menuRegister(), cfg.getRegisterUrl()));
}
- signInAnchor = anchor(C.menuSignIn(), loginRedirect(History.getToken()));
- menuRight.add(signInAnchor);
+ menuRight.addItem(C.menuSignIn(), new Command() {
+ public void execute() {
+ doSignIn(History.getToken());
+ }
+ });
break;
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index ee107d07b3..87c01ccd61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -21,6 +21,7 @@ public interface GerritConstants extends Constants {
String menuSignOut();
String menuRegister();
String menuSettings();
+ String reportBug();
String signInDialogTitle();
String signInDialogClose();
@@ -60,6 +61,7 @@ public interface GerritConstants extends Constants {
String menuMyDrafts();
String menuMyWatchedChanges();
String menuMyStarredChanges();
+ String menuMyDraftComments();
String menuDiff();
String menuDiffCommit();
@@ -67,10 +69,16 @@ public interface GerritConstants extends Constants {
String menuDiffPatchSets();
String menuDiffFiles();
- String menuAdmin();
+ String menuProjects();
+ String menuProjectsList();
+ String menuProjectsCreate();
+
String menuPeople();
String menuGroups();
- String menuProjects();
+ String menuGroupsList();
+ String menuGroupsCreate();
+ String menuPlugins();
+ String menuPluginsInstalled();
String menuDocumentation();
String menuDocumentationIndex();
@@ -96,4 +104,8 @@ public interface GerritConstants extends Constants {
String jumpMineDrafts();
String jumpMineWatched();
String jumpMineStarred();
+ String jumpMineDraftComments();
+
+ String projectAccessError();
+ String projectAccessProposeForReviewHint();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 41db3d5de1..596b3adb0c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -2,6 +2,7 @@ menuSignIn = Sign In
menuSignOut = Sign Out
menuRegister = Register
menuSettings = Settings
+reportBug = Report Bug
signInDialogTitle = Code Review - Sign In
signInDialogClose = Close
@@ -43,6 +44,7 @@ menuMyChanges = Changes
menuMyDrafts = Drafts
menuMyStarredChanges = Starred Changes
menuMyWatchedChanges = Watched Changes
+menuMyDraftComments = Draft Comments
menuDiff = Differences
menuDiffCommit = Commit Message
@@ -50,10 +52,17 @@ menuDiffPreferences = Preferences
menuDiffPatchSets = Patch Sets
menuDiffFiles = Files
-menuAdmin = Admin
+menuProjects = Projects
+menuProjectsList = List
+menuProjectsCreate = Create New Project
+
menuPeople = People
menuGroups = Groups
-menuProjects = Projects
+menuGroupsList = List
+menuGroupsCreate = Create New Group
+
+menuPlugins = Plugins
+menuPluginsInstalled = Installed
menuDocumentation = Documentation
menuDocumentationIndex = Index
@@ -79,3 +88,7 @@ jumpMine = Go to my dashboard
jumpMineWatched = Go to watched changes
jumpMineDrafts = Go to drafts
jumpMineStarred = Go to starred changes
+jumpMineDraftComments = Go to draft comments
+
+projectAccessError = You don't have permissions to modify the access rights for the following refs:
+projectAccessProposeForReviewHint = You may propose these modifications to the project owners by clicking on 'Save for Review'.
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 a8315d8b79..9cbf5cd1b4 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
@@ -78,7 +78,7 @@ public interface GerritCss extends CssResource {
String contributorAgreementLegal();
String contributorAgreementShortDescription();
String coverMessage();
- String createProjectLink();
+ String createGroupLink();
String createProjectPanel();
String dataCell();
String dataHeader();
@@ -104,6 +104,7 @@ public interface GerritCss extends CssResource {
String errorDialogTitle();
String errorDialogButtons();
String errorDialogErrorType();
+ String errorDialogText();
String fileColumnHeader();
String fileLine();
String fileLineCONTEXT();
@@ -167,7 +168,7 @@ public interface GerritCss extends CssResource {
String patchSetRevision();
String patchSetUserIdentity();
String patchSizeCell();
- String permalink();
+ String pluginsTable();
String posscore();
String projectAdminApprovalCategoryRangeLine();
String projectAdminApprovalCategoryValue();
@@ -181,6 +182,7 @@ public interface GerritCss extends CssResource {
String rpcStatusPanel();
String screen();
String screenHeader();
+ String screenNoHeader();
String searchPanel();
String sectionHeader();
String sideBySideScreenLinkTable();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index a91df3c9cd..79772bd45c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -1,7 +1,6 @@
windowTitle1 = {0} Code Review
windowTitle2 = {0} | {1} Code Review
-poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0}) \
-| <a href="http://code.google.com/p/gerrit/issues/list" target="_blank">Report Bug</a>
+poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0})
noSuchAccountMessage = {0} is not a registered user.
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 b5c0900675..d763ff1c1c 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
@@ -39,4 +39,7 @@ public interface GerritResources extends ClientBundle {
@Source("redNot.png")
public ImageResource redNot();
+
+ @Source("downloadIcon.png")
+ public ImageResource downloadIcon();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index 873045dd44..a41ff02756 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -55,6 +55,12 @@ class JumpKeys {
jumps.add(new KeyCommand(0, 'd', Gerrit.C.jumpMineDrafts()) {
@Override
public void onKeyPress(final KeyPressEvent event) {
+ Gerrit.display(PageLinks.toChangeQuery("is:draft"));
+ }
+ });
+ jumps.add(new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) {
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
Gerrit.display(PageLinks.toChangeQuery("has:draft"));
}
});
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 76ce3844e0..955c8e2648 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -56,6 +56,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
@Override
public void onRpcStart(final RpcStartEvent event) {
+ onRpcStart();
+ }
+
+ public void onRpcStart() {
if (++activeCalls == 1) {
if (hideDepth == 0) {
loading.setVisible(true);
@@ -65,6 +69,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
@Override
public void onRpcComplete(final RpcCompleteEvent event) {
+ onRpcComplete();
+ }
+
+ public void onRpcComplete() {
if (--activeCalls == 0) {
loading.setVisible(false);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index e3d8468be2..7e7b9276b7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -27,6 +27,8 @@ import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -42,15 +44,22 @@ class SearchPanel extends Composite {
searchBox = new HintTextBox();
searchBox.setVisibleLength(70);
searchBox.setHintText(Gerrit.C.searchHint());
+ final MySuggestionDisplay suggestionDisplay = new MySuggestionDisplay();
searchBox.addKeyPressHandler(new KeyPressHandler() {
@Override
public void onKeyPress(final KeyPressEvent event) {
if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
- doSearch();
+ if (!suggestionDisplay.isSuggestionSelected) {
+ doSearch();
+ }
}
}
});
+ final SuggestBox suggestBox =
+ new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
+ searchBox.setStyleName("gwt-TextBox");
+
final Button searchButton = new Button(Gerrit.C.searchButton());
searchButton.addClickHandler(new ClickHandler() {
@Override
@@ -59,7 +68,7 @@ class SearchPanel extends Composite {
}
});
- body.add(searchBox);
+ body.add(suggestBox);
body.add(searchButton);
}
@@ -106,4 +115,15 @@ class SearchPanel extends Composite {
Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
}
}
+
+ private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
+ private boolean isSuggestionSelected;
+
+ @Override
+ protected Suggestion getCurrentSelection() {
+ Suggestion currentSelection = super.getCurrentSelection();
+ isSuggestionSelected = currentSelection != null;
+ return currentSelection;
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
new file mode 100644
index 0000000000..172a2af901
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -0,0 +1,140 @@
+// 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.client;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.TreeSet;
+
+public class SearchSuggestOracle extends HighlightSuggestOracle {
+ private static final TreeSet<String> suggestions = new TreeSet<String>();
+
+ static {
+ suggestions.add("age:");
+ suggestions.add("age:1week"); // Give an example age
+
+ suggestions.add("change:");
+
+ suggestions.add("owner:");
+ suggestions.add("owner:self");
+ suggestions.add("ownerin:");
+
+ suggestions.add("reviewer:");
+ suggestions.add("reviewer:self");
+ suggestions.add("reviewerin:");
+
+ suggestions.add("commit:");
+ suggestions.add("project:");
+ suggestions.add("branch:");
+ suggestions.add("topic:");
+ suggestions.add("ref:");
+ suggestions.add("tr:");
+ suggestions.add("bug:");
+ suggestions.add("label:");
+ suggestions.add("message:");
+ suggestions.add("file:");
+
+ suggestions.add("has:");
+ suggestions.add("has:draft");
+ suggestions.add("has:star");
+
+ suggestions.add("is:");
+ suggestions.add("is:starred");
+ suggestions.add("is:watched");
+ suggestions.add("is:reviewed");
+ suggestions.add("is:owner");
+ suggestions.add("is:reviewer");
+ suggestions.add("is:open");
+ suggestions.add("is:draft");
+ suggestions.add("is:closed");
+ suggestions.add("is:submitted");
+ suggestions.add("is:merged");
+ suggestions.add("is:abandoned");
+
+ suggestions.add("status:");
+ suggestions.add("status:open");
+ suggestions.add("status:reviewed");
+ suggestions.add("status:submitted");
+ suggestions.add("status:closed");
+ suggestions.add("status:merged");
+ suggestions.add("status:abandoned");
+
+ suggestions.add("AND");
+ suggestions.add("OR");
+ suggestions.add("NOT");
+ }
+
+ @Override
+ public void requestDefaultSuggestions(Request request, Callback done) {
+ final ArrayList<SearchSuggestion> r = new ArrayList<SearchSuggestOracle.SearchSuggestion>();
+ // No text - show some default suggestions.
+ r.add(new SearchSuggestion("status:open", "status:open"));
+ r.add(new SearchSuggestion("age:1week", "age:1week"));
+ if (Gerrit.isSignedIn()) {
+ r.add(new SearchSuggestion("owner:self", "owner:self"));
+ }
+ done.onSuggestionsReady(request, new Response(r));
+ }
+
+ @Override
+ protected void onRequestSuggestions(Request request, Callback done) {
+ final String query = request.getQuery();
+ int lastSpace = query.lastIndexOf(' ');
+ final String lastWord;
+ // NOTE: this method is not called if the query is empty.
+ if (lastSpace == query.length() - 1) {
+ // Starting a new word - don't show suggestions yet.
+ done.onSuggestionsReady(request, null);
+ return;
+ } else if (lastSpace == -1) {
+ lastWord = query;
+ } else {
+ lastWord = query.substring(lastSpace + 1);
+ }
+
+ final ArrayList<SearchSuggestion> r = new ArrayList<SearchSuggestOracle.SearchSuggestion>();
+ for (String suggestion : suggestions.tailSet(lastWord)) {
+ if ((lastWord.length() < suggestion.length()) && suggestion.startsWith(lastWord)) {
+ if (suggestion.contains("self") && !Gerrit.isSignedIn()) {
+ continue;
+ }
+ r.add(new SearchSuggestion(suggestion, query + suggestion.substring(lastWord.length())));
+ }
+ }
+ done.onSuggestionsReady(request, new Response(r));
+ }
+
+ private static class SearchSuggestion implements SuggestOracle.Suggestion {
+ private final String suggestion;
+ private final String fullQuery;
+ public SearchSuggestion(String suggestion, String fullQuery) {
+ this.suggestion = suggestion;
+ // Add a space to the query if it is a complete operation (e.g.
+ // "status:open") so the user can keep on typing.
+ this.fullQuery = fullQuery.endsWith(":") ? fullQuery : fullQuery + " ";
+ }
+ @Override
+ public String getDisplayString() {
+ return suggestion;
+ }
+
+ @Override
+ public String getReplacementString() {
+ return fullQuery;
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
new file mode 100644
index 0000000000..0565d3e37a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
@@ -0,0 +1,36 @@
+// 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.client.account;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Capabilities the caller has from {@code /accounts/self/capabilities}. */
+public class AccountCapabilities extends JavaScriptObject {
+ public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
+ RestApi api = new RestApi("/accounts/self/capabilities");
+ for (String name : filter) {
+ api.addParameter("q", name);
+ }
+ api.send(cb);
+ }
+
+ protected AccountCapabilities() {
+ }
+
+ public final native boolean canPerform(String name)
+ /*-{ return this[name] ? true : false; }-*/;
+}
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 c886216778..d374d352fe 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
@@ -30,8 +30,8 @@ public interface AccountConstants extends Constants {
String showSiteHeader();
String useFlashClipboard();
String copySelfOnEmails();
- String displayPatchSetsInReverseOrder();
- String displayPersonNameInReviewCategory();
+ String reversePatchSetOrder();
+ String showUsernameInReviewCategory();
String buttonSaveChanges();
String tabAccountSummary();
@@ -57,6 +57,8 @@ public interface AccountConstants extends Constants {
String buttonClearPassword();
String buttonGeneratePassword();
String linkObtainPassword();
+ String linkEditFullName();
+ String linkReloadContact();
String invalidUserName();
String invalidUserEmail();
@@ -113,10 +115,7 @@ public interface AccountConstants extends Constants {
String agreementStatus();
String agreementName();
String agreementDescription();
- String agreementAccepted();
String agreementStatus_EXPIRED();
- String agreementStatus_NEW();
- String agreementStatus_REJECTED();
String agreementStatus_VERIFIED();
String newAgreementSelectTypeHeading();
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 499f051b9a..8b32174c3c 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
@@ -7,8 +7,8 @@ accountId = Account ID
showSiteHeader = Show Site Header
useFlashClipboard = Use Flash Clipboard Widget
copySelfOnEmails = CC Me On Comments I Write
-displayPatchSetsInReverseOrder = Display Patch Sets In Reverse Order
-displayPersonNameInReviewCategory = Display Person Name In Review Category
+reversePatchSetOrder = Display Patch Sets In Reverse Order
+showUsernameInReviewCategory = Display Person Name In Review Category
defaultContextFieldLabel = Default Context:
maximumPageSizeFieldLabel = Maximum Page Size:
dateFormatLabel = Date/Time Format:
@@ -38,6 +38,8 @@ buttonChangeUserName = Change Username
buttonClearPassword = Clear Password
buttonGeneratePassword = Generate Password
linkObtainPassword = Obtain Password
+linkEditFullName = Edit
+linkReloadContact = Reload
invalidUserName = Username must contain only letters, numbers, _, - or .
invalidUserEmail = Email format is wrong.
sshKeyInvalid = Invalid Key
@@ -103,11 +105,8 @@ newAgreement = New Contributor Agreement
agreementStatus = Status
agreementName = Name
agreementStatus_EXPIRED = Expired
-agreementStatus_NEW = Pending
-agreementStatus_REJECTED = Rejected
agreementStatus_VERIFIED = Verified
agreementDescription = Description
-agreementAccepted = Accepted
newAgreementSelectTypeHeading = Select an agreement type:
newAgreementNoneAvailable = No contributor agreements are configured.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 4e0b3b2147..4fbe7a0f3d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -18,17 +18,18 @@ import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
@@ -41,6 +42,7 @@ import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
import java.util.ArrayList;
import java.util.Collections;
@@ -102,12 +104,37 @@ class ContactPanelShort extends Composite {
}
int row = 0;
- if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+ if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+ && Gerrit.getConfig().siteHasUsernames()) {
infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
row(infoPlainText, row++, Util.C.userName(), new UsernameField());
}
- row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+ if (!canEditFullName()) {
+ FlowPanel nameLine = new FlowPanel();
+ nameLine.add(nameTxt);
+ if (Gerrit.getConfig().getEditFullNameUrl() != null) {
+ Button edit = new Button(Util.C.linkEditFullName());
+ edit.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ Window.open(Gerrit.getConfig().getEditFullNameUrl(), "_blank", null);
+ }
+ });
+ nameLine.add(edit);
+ }
+ Button reload = new Button(Util.C.linkReloadContact());
+ reload.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ Window.Location.replace(Gerrit.loginRedirect(PageLinks.SETTINGS_CONTACT));
+ }
+ });
+ nameLine.add(reload);
+ row(infoPlainText, row++, Util.C.contactFieldFullName(), nameLine);
+ } else {
+ row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+ }
row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index af619a8fa9..3bd2e77844 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -14,21 +14,15 @@
package com.google.gerrit.client.account;
-import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
public class MyAgreementsScreen extends SettingsScreen {
private AgreementTable agreements;
@@ -52,16 +46,15 @@ public class MyAgreementsScreen extends SettingsScreen {
});
}
- private class AgreementTable extends FancyFlexTable<AbstractAgreement> {
+ private class AgreementTable extends FancyFlexTable<ContributorAgreement> {
AgreementTable() {
table.setWidth("");
table.setText(0, 1, Util.C.agreementStatus());
table.setText(0, 2, Util.C.agreementName());
table.setText(0, 3, Util.C.agreementDescription());
- table.setText(0, 4, Util.C.agreementAccepted());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
- for (int c = 1; c <= 4; c++) {
+ for (int c = 1; c < 4; c++) {
fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
}
}
@@ -70,37 +63,22 @@ public class MyAgreementsScreen extends SettingsScreen {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final AccountAgreement k : result.userAccepted) {
- addOne(result, k);
- }
- for (final AccountGroupAgreement k : result.groupAccepted) {
+ for (final String k : result.accepted) {
addOne(result, k);
}
}
- void addOne(final AgreementInfo info, final AbstractAgreement k) {
+ void addOne(final AgreementInfo info, final String k) {
final int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
- final ContributorAgreement cla = info.agreements.get(k.getAgreementId());
+ final ContributorAgreement cla = info.agreements.get(k);
final String statusName;
- if (cla == null || !cla.isActive()) {
+ if (cla == null) {
statusName = Util.C.agreementStatus_EXPIRED();
} else {
- switch (k.getStatus()) {
- case NEW:
- statusName = Util.C.agreementStatus_NEW();
- break;
- case REJECTED:
- statusName = Util.C.agreementStatus_REJECTED();
- break;
- case VERIFIED:
- statusName = Util.C.agreementStatus_VERIFIED();
- break;
- default:
- statusName = k.getStatus().name();
- }
+ statusName = Util.C.agreementStatus_VERIFIED();
}
table.setText(row, 1, statusName);
@@ -110,28 +88,20 @@ public class MyAgreementsScreen extends SettingsScreen {
} else {
final String url = cla.getAgreementUrl();
if (url != null && url.length() > 0) {
- final Anchor a = new Anchor(cla.getShortName(), url);
+ final Anchor a = new Anchor(cla.getName(), url);
a.setTarget("_blank");
table.setWidget(row, 2, a);
} else {
- table.setText(row, 2, cla.getShortName());
+ table.setText(row, 2, cla.getName());
}
- table.setText(row, 3, cla.getShortDescription());
+ table.setText(row, 3, cla.getDescription());
}
-
- final SafeHtmlBuilder b = new SafeHtmlBuilder();
- b.append(FormatUtil.mediumFormat(k.getAcceptedOn()));
- b.br();
- b.append(FormatUtil.mediumFormat(k.getReviewedOn()));
- SafeHtml.set(table, row, 4, b);
-
final FlexCellFormatter fmt = table.getFlexCellFormatter();
- for (int c = 1; c <= 4; c++) {
+ for (int c = 1; c < 4; c++) {
fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
}
- fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().cLastUpdate());
- setRowItem(row, k);
+ setRowItem(row, cla);
}
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index 719c3952c2..6cb749d40f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -16,7 +16,7 @@ package com.google.gerrit.client.account;
import com.google.gerrit.client.admin.GroupTable;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import java.util.List;
@@ -33,8 +33,9 @@ public class MyGroupsScreen extends SettingsScreen {
@Override
protected void onLoad() {
super.onLoad();
- Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<GroupDetail>>(this) {
- public void preDisplay(final List<GroupDetail> result) {
+ Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+ @Override
+ public void preDisplay(final List<AccountGroup> result) {
groups.display(result);
}
});
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 84e87aaa1b..de6e666912 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
@@ -41,8 +41,8 @@ public class MyPreferencesScreen extends SettingsScreen {
private CheckBox showSiteHeader;
private CheckBox useFlashClipboard;
private CheckBox copySelfOnEmails;
- private CheckBox displayPatchSetsInReverseOrder;
- private CheckBox displayPersonNameInReviewCategory;
+ private CheckBox reversePatchSetOrder;
+ private CheckBox showUsernameInReviewCategory;
private ListBox maximumPageSize;
private ListBox dateFormat;
private ListBox timeFormat;
@@ -74,11 +74,11 @@ public class MyPreferencesScreen extends SettingsScreen {
copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
copySelfOnEmails.addClickHandler(onClickSave);
- displayPatchSetsInReverseOrder = new CheckBox(Util.C.displayPatchSetsInReverseOrder());
- displayPatchSetsInReverseOrder.addClickHandler(onClickSave);
+ reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
+ reversePatchSetOrder.addClickHandler(onClickSave);
- displayPersonNameInReviewCategory = new CheckBox(Util.C.displayPersonNameInReviewCategory());
- displayPersonNameInReviewCategory.addClickHandler(onClickSave);
+ showUsernameInReviewCategory = new CheckBox(Util.C.showUsernameInReviewCategory());
+ showUsernameInReviewCategory.addClickHandler(onClickSave);
maximumPageSize = new ListBox();
for (final short v : PAGESIZE_CHOICES) {
@@ -137,11 +137,11 @@ public class MyPreferencesScreen extends SettingsScreen {
row++;
formGrid.setText(row, labelIdx, "");
- formGrid.setWidget(row, fieldIdx, displayPatchSetsInReverseOrder);
+ formGrid.setWidget(row, fieldIdx, reversePatchSetOrder);
row++;
formGrid.setText(row, labelIdx, "");
- formGrid.setWidget(row, fieldIdx, displayPersonNameInReviewCategory);
+ formGrid.setWidget(row, fieldIdx, showUsernameInReviewCategory);
row++;
formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
@@ -179,8 +179,8 @@ public class MyPreferencesScreen extends SettingsScreen {
showSiteHeader.setEnabled(on);
useFlashClipboard.setEnabled(on);
copySelfOnEmails.setEnabled(on);
- displayPatchSetsInReverseOrder.setEnabled(on);
- displayPersonNameInReviewCategory.setEnabled(on);
+ reversePatchSetOrder.setEnabled(on);
+ showUsernameInReviewCategory.setEnabled(on);
maximumPageSize.setEnabled(on);
dateFormat.setEnabled(on);
timeFormat.setEnabled(on);
@@ -190,8 +190,8 @@ public class MyPreferencesScreen extends SettingsScreen {
showSiteHeader.setValue(p.isShowSiteHeader());
useFlashClipboard.setValue(p.isUseFlashClipboard());
copySelfOnEmails.setValue(p.isCopySelfOnEmails());
- displayPatchSetsInReverseOrder.setValue(p.isDisplayPatchSetsInReverseOrder());
- displayPersonNameInReviewCategory.setValue(p.isDisplayPersonNameInReviewCategory());
+ reversePatchSetOrder.setValue(p.isReversePatchSetOrder());
+ showUsernameInReviewCategory.setValue(p.isShowUsernameInReviewCategory());
setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
p.getDateFormat());
@@ -251,8 +251,8 @@ public class MyPreferencesScreen extends SettingsScreen {
p.setShowSiteHeader(showSiteHeader.getValue());
p.setUseFlashClipboard(useFlashClipboard.getValue());
p.setCopySelfOnEmails(copySelfOnEmails.getValue());
- p.setDisplayPatchSetsInReverseOrder(displayPatchSetsInReverseOrder.getValue());
- p.setDisplayPersonNameInReviewCategory(displayPersonNameInReviewCategory.getValue());
+ p.setReversePatchSetOrder(reversePatchSetOrder.getValue());
+ p.setShowUsernameInReviewCategory(showUsernameInReviewCategory.getValue());
p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
p.setDateFormat(getListBox(dateFormat,
AccountGeneralPreferences.DateFormat.STD,
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 165168d8d0..ae04e0aec7 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
@@ -38,21 +38,24 @@ public class MyProfileScreen extends SettingsScreen {
fieldIdx = 1;
}
- info = new Grid(5, 2);
+ info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
add(info);
- infoRow(0, Util.C.userName());
- infoRow(1, Util.C.fullName());
- infoRow(2, Util.C.preferredEmail());
- infoRow(3, Util.C.registeredOn());
- infoRow(4, Util.C.accountId());
+ int row = 0;
+ if (Gerrit.getConfig().siteHasUsernames()) {
+ infoRow(row++, Util.C.userName());
+ }
+ infoRow(row++, Util.C.fullName());
+ infoRow(row++, Util.C.preferredEmail());
+ infoRow(row++, Util.C.registeredOn());
+ infoRow(row++, Util.C.accountId());
final CellFormatter fmt = info.getCellFormatter();
fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
- fmt.addStyleName(4, 0, Gerrit.RESOURCES.css().bottomheader());
+ fmt.addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
}
@Override
@@ -69,10 +72,13 @@ public class MyProfileScreen extends SettingsScreen {
}
void display(final Account account) {
- info.setWidget(0, fieldIdx, new UsernameField());
- info.setText(1, fieldIdx, account.getFullName());
- info.setText(2, fieldIdx, account.getPreferredEmail());
- info.setText(3, fieldIdx, mediumFormat(account.getRegisteredOn()));
- info.setText(4, fieldIdx, account.getId().toString());
+ int row = 0;
+ if (Gerrit.getConfig().siteHasUsernames()) {
+ info.setWidget(row++, fieldIdx, new UsernameField());
+ }
+ info.setText(row++, fieldIdx, account.getFullName());
+ info.setText(row++, fieldIdx, account.getPreferredEmail());
+ info.setText(row++, fieldIdx, mediumFormat(account.getRegisteredOn()));
+ info.setText(row++, fieldIdx, account.getId().toString());
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 5a62eac1a0..d38015c425 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -18,70 +18,46 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.ProjectListPopup;
import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
-import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.common.data.ProjectList;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
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.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
-import com.google.gwtexpui.user.client.PluginSafeDialogBox;
import java.util.List;
-public class MyWatchedProjectsScreen extends SettingsScreen implements
- ResizeHandler {
+public class MyWatchedProjectsScreen extends SettingsScreen {
private Button addNew;
private HintTextBox nameBox;
private SuggestBox nameTxt;
private HintTextBox filterTxt;
private MyWatchesTable watchesTab;
private Button browse;
- private PluginSafeDialogBox popup;
- private Button close;
- private ProjectsTable projectsTab;
private Button delSel;
-
- private PopupPanel.PositionCallback popupPosition;
- private HandlerRegistration regWindowResize;
-
- private int preferredPopupWidth = -1;
-
private boolean submitOnSelection;
- private boolean firstPopupLoad = true;
- private boolean popingUp;
-
- private ScrollPanel sp;
+ private Grid grid;
+ private ProjectListPopup projectsPopup;
@Override
protected void onInitUI() {
super.onInitUI();
createWidgets();
-
/* top table */
-
- final Grid grid = new Grid(2, 2);
+ grid = new Grid(2, 2);
grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
grid.setText(0, 0, Util.C.watchedProjectName());
grid.setWidget(0, 1, nameTxt);
@@ -105,62 +81,27 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
/* bottom table */
-
add(watchesTab);
add(delSel);
/* popup */
-
- final FlowPanel pfp = new FlowPanel();
- sp = new ScrollPanel(projectsTab);
- pfp.add(sp);
- pfp.add(close);
- popup.setWidget(pfp);
-
- popupPosition = new PopupPanel.PositionCallback() {
- public void setPosition(int offsetWidth, int offsetHeight) {
- if (preferredPopupWidth == -1) {
- preferredPopupWidth = offsetWidth;
- }
-
- int top = grid.getAbsoluteTop() - 50; // under page header
-
- // Try to place it to the right of everything else, but not
- // right justified
- int left = 5 + Math.max(
- grid.getAbsoluteLeft() + grid.getOffsetWidth(),
- watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth() );
-
- if (top + offsetHeight > Window.getClientHeight()) {
- top = Window.getClientHeight() - offsetHeight;
- }
- if (left + offsetWidth > Window.getClientWidth()) {
- left = Window.getClientWidth() - offsetWidth;
- }
-
- if (top < 0) {
- sp.setHeight((sp.getOffsetHeight() + top) + "px");
- top = 0;
- }
- if (left < 0) {
- sp.setWidth((sp.getOffsetWidth() + left) + "px");
- left = 0;
+ projectsPopup = new ProjectListPopup() {
+ @Override
+ protected void onMovePointerTo(String projectName) {
+ // prevent user input from being overwritten by simply poping up
+ if (!projectsPopup.isPopingUp() || "".equals(nameBox.getText())) {
+ nameBox.setText(projectName);
}
+ }
- popup.setPopupPosition(left, top);
+ @Override
+ protected void openRow(String projectName) {
+ nameBox.setText(projectName);
+ doAddNew();
}
};
- }
-
- @Override
- public void onResize(final ResizeEvent event) {
- sp.setSize("100%","100%");
-
- // For some reason keeping track of preferredWidth keeps the width better,
- // but using 100% for height works better.
- popup.setHeight("100%");
- popupPosition.setPosition(preferredPopupWidth, popup.getOffsetHeight());
+ projectsPopup.initPopup(Util.C.projects(), PageLinks.SETTINGS_PROJECTS);
}
protected void createWidgets() {
@@ -213,49 +154,18 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
}
});
- projectsTab = new ProjectsTable() {
- {
- keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
- keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
- Util.C.projectListOpen()));
- }
-
- @Override
- protected void movePointerTo(final int row, final boolean scroll) {
- super.movePointerTo(row, scroll);
-
- // prevent user input from being overwritten by simply poping up
- if (! popingUp || "".equals(nameBox.getText()) ) {
- nameBox.setText(getRowItem(row).getName());
- }
- }
-
- @Override
- protected void onOpenRow(final int row) {
- super.onOpenRow(row);
- nameBox.setText(getRowItem(row).getName());
- doAddNew();
- }
- };
- projectsTab.setSavePointerId(PageLinks.SETTINGS_PROJECTS);
-
- close = new Button(Util.C.projectsClose());
- close.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(final ClickEvent event) {
- closePopup();
- }
- });
-
- popup = new PluginSafeDialogBox();
- popup.setModal(false);
- popup.setText(Util.C.projects());
-
browse = new Button(Util.C.buttonBrowseProjects());
browse.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
- displayPopup();
+ int top = grid.getAbsoluteTop() - 50; // under page header
+ // Try to place it to the right of everything else, but not
+ // right justified
+ int left =
+ 5 + Math.max(grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+ watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth());
+ projectsPopup.setPreferredCoordinates(top, left);
+ projectsPopup.displayPopup();
}
});
@@ -279,37 +189,7 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
@Override
protected void onUnload() {
super.onUnload();
- closePopup();
- }
-
- protected void displayPopup() {
- popingUp = true;
- if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
- populateProjects();
- } else {
- popup.setPopupPositionAndShow(popupPosition);
-
- GlobalKey.dialog(popup);
- GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
- KeyCodes.KEY_ESCAPE, popup));
- projectsTab.setRegisterKeys(true);
-
- projectsTab.finishDisplay();
-
- if (regWindowResize == null) {
- regWindowResize = Window.addResizeHandler(this);
- }
-
- popingUp = false;
- }
- }
-
- protected void closePopup() {
- popup.hide();
- if (regWindowResize != null) {
- regWindowResize.removeHandler();
- regWindowResize = null;
- }
+ projectsPopup.closePopup();
}
protected void doAddNew() {
@@ -359,18 +239,4 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
}
});
}
-
- protected void populateProjects() {
- Util.PROJECT_SVC.visibleProjects(
- new GerritCallback<ProjectList>() {
- @Override
- public void onSuccess(final ProjectList result) {
- projectsTab.display(result.getProjects());
- if (firstPopupLoad) { // Display was delayed until table was loaded
- firstPopupLoad = false;
- displayPopup();
- }
- }
- });
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 154e2ceb8c..ac44dbebc7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -22,10 +22,8 @@ import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -53,7 +51,7 @@ import java.util.Set;
public class NewAgreementScreen extends AccountScreen {
private final String nextToken;
- private Set<ContributorAgreement.Id> mySigned;
+ private Set<String> mySigned;
private List<ContributorAgreement> available;
private ContributorAgreement current;
@@ -83,13 +81,7 @@ public class NewAgreementScreen extends AccountScreen {
Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
public void onSuccess(AgreementInfo result) {
if (isAttached()) {
- mySigned = new HashSet<ContributorAgreement.Id>();
- for (AccountAgreement a : result.userAccepted) {
- mySigned.add(a.getAgreementId());
- }
- for (AccountGroupAgreement a : result.groupAccepted) {
- mySigned.add(a.getAgreementId());
- }
+ mySigned = new HashSet<String>(result.accepted);
postRPC();
}
}
@@ -176,11 +168,11 @@ public class NewAgreementScreen extends AccountScreen {
radios.add(hdr);
for (final ContributorAgreement cla : available) {
- final RadioButton r = new RadioButton("cla_id", cla.getShortName());
+ final RadioButton r = new RadioButton("cla_id", cla.getName());
r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
radios.add(r);
- if (mySigned.contains(cla.getId())) {
+ if (mySigned.contains(cla.getName())) {
r.setEnabled(false);
final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
@@ -194,9 +186,8 @@ public class NewAgreementScreen extends AccountScreen {
});
}
- if (cla.getShortDescription() != null
- && !cla.getShortDescription().equals("")) {
- final Label l = new Label(cla.getShortDescription());
+ if (cla.getDescription() != null && !cla.getDescription().equals("")) {
+ final Label l = new Label(cla.getDescription());
l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
radios.add(l);
}
@@ -231,7 +222,7 @@ public class NewAgreementScreen extends AccountScreen {
}
private void doEnterAgreement() {
- Util.ACCOUNT_SEC.enterAgreement(current.getId(),
+ Util.ACCOUNT_SEC.enterAgreement(current.getName(),
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
Gerrit.display(nextToken);
@@ -284,8 +275,9 @@ public class NewAgreementScreen extends AccountScreen {
contactGroup.add(contactPanel);
contactPanel.hideSaveButton();
}
- contactGroup.setVisible(cla.isRequireContactInformation());
- finalGroup.setVisible(true);
+ contactGroup.setVisible(
+ cla.isRequireContactInformation() && cla.getAutoVerify() != null);
+ finalGroup.setVisible(cla.getAutoVerify() != null);
yesIAgreeBox.setText("");
submit.setEnabled(false);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index 084ea6f89f..2810931e80 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -125,7 +125,7 @@ public class RegisterScreen extends AccountScreen {
agreementGroup.add(whyAgreement);
choices.add(new InlineHyperlink(Util.C.newAgreement(),
- PageLinks.SETTINGS_NEW_AGREEMENT + "," + nextToken));
+ PageLinks.SETTINGS_NEW_AGREEMENT));
choices
.add(new InlineHyperlink(Util.C.welcomeAgreementLater(), nextToken));
formBody.add(agreementGroup);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 72ac1a448b..a9aa418306 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -182,7 +182,7 @@ public class AccessSectionEditor extends Composite implements
Collections.sort(value.getPermissions());
this.value = value;
- this.readOnly = !editing || !projectAccess.isOwnerOf(value);
+ this.readOnly = !editing || !(projectAccess.isOwnerOf(value) || projectAccess.canUpload());
name.setEnabled(!readOnly);
deleteSection.setVisible(!readOnly);
@@ -223,22 +223,16 @@ public class AccessSectionEditor extends Composite implements
if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
for (String varName : Util.C.capabilityNames().keySet()) {
- if (value.getPermission(varName) == null) {
- perms.add(varName);
- }
+ addPermission(varName, perms);
}
} else if (RefConfigSection.isValid(value.getName())) {
for (ApprovalType t : Gerrit.getConfig().getApprovalTypes()
.getApprovalTypes()) {
String varName = Permission.LABEL + t.getCategory().getLabelName();
- if (value.getPermission(varName) == null) {
- perms.add(varName);
- }
+ addPermission(varName, perms);
}
for (String varName : Util.C.permissionNames().keySet()) {
- if (value.getPermission(varName) == null) {
- perms.add(varName);
- }
+ addPermission(varName, perms);
}
}
if (perms.isEmpty()) {
@@ -251,6 +245,19 @@ public class AccessSectionEditor extends Composite implements
}
}
+ private void addPermission(final String permissionName,
+ final List<String> permissionList) {
+ if (value.getPermission(permissionName) != null) {
+ return;
+ }
+ if (Gerrit.getConfig().getWildProject()
+ .equals(projectAccess.getProjectName())
+ && !Permission.canBeOnAllProjects(value.getName(), permissionName)) {
+ return;
+ }
+ permissionList.add(permissionName);
+ }
+
@Override
public void flush() {
List<Permission> src = permissions.getList();
@@ -276,7 +283,8 @@ public class AccessSectionEditor extends Composite implements
private class PermissionEditorSource extends EditorSource<PermissionEditor> {
@Override
public PermissionEditor create(int index) {
- PermissionEditor subEditor = new PermissionEditor(readOnly, value);
+ PermissionEditor subEditor =
+ new PermissionEditor(projectAccess.getProjectName(), readOnly, value);
permissionContainer.insert(subEditor, index);
return subEditor;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index 936bfe5b90..ea7c04b1a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -27,17 +27,10 @@ import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
-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.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -45,8 +38,6 @@ import com.google.gwtexpui.globalkey.client.NpTextArea;
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.List;
-
public class AccountGroupInfoScreen extends AccountGroupScreen {
private CopyableLabel groupUUIDLabel;
@@ -64,12 +55,6 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
private ListBox typeSelect;
private Button saveType;
- private Panel externalPanel;
- private Label externalName;
- private NpTextBox externalNameFilter;
- private Button externalNameSearch;
- private Grid externalMatches;
-
private CheckBox visibleToAllCheckBox;
private Button saveGroupOptions;
@@ -86,8 +71,6 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
initDescription();
initGroupOptions();
initGroupType();
-
- initExternal();
}
private void enableForm(final boolean canModify) {
@@ -95,8 +78,6 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
ownerTxtBox.setEnabled(canModify);
descTxt.setEnabled(canModify);
typeSelect.setEnabled(canModify);
- externalNameFilter.setEnabled(canModify);
- externalNameSearch.setEnabled(canModify);
visibleToAllCheckBox.setEnabled(canModify);
}
@@ -243,7 +224,6 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
typeSelect = new ListBox();
typeSelect.setStyleName(Gerrit.RESOURCES.css().groupTypeSelectListBox());
typeSelect.addItem(Util.C.groupType_INTERNAL(), AccountGroup.Type.INTERNAL.name());
- typeSelect.addItem(Util.C.groupType_LDAP(), AccountGroup.Type.LDAP.name());
typeSelect.addChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
@@ -279,54 +259,12 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
add(fp);
}
- private void initExternal() {
- externalName = new Label();
-
- externalNameFilter = new NpTextBox();
- externalNameFilter.setStyleName(Gerrit.RESOURCES.css()
- .groupExternalNameFilterTextBox());
- externalNameFilter.setVisibleLength(30);
- externalNameFilter.addKeyPressHandler(new KeyPressHandler() {
- @Override
- public void onKeyPress(final KeyPressEvent event) {
- if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
- doExternalSearch();
- }
- }
- });
-
- externalNameSearch = new Button(Gerrit.C.searchButton());
- externalNameSearch.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(ClickEvent event) {
- doExternalSearch();
- }
- });
-
- externalMatches = new Grid();
- externalMatches.setStyleName(Gerrit.RESOURCES.css().infoTable());
- externalMatches.setVisible(false);
-
- final FlowPanel searchLine = new FlowPanel();
- searchLine.add(externalNameFilter);
- searchLine.add(externalNameSearch);
-
- externalPanel = new VerticalPanel();
- externalPanel.add(new SmallHeading(Util.C.headingExternalGroup()));
- externalPanel.add(externalName);
- externalPanel.add(searchLine);
- externalPanel.add(externalMatches);
- add(externalPanel);
- }
-
private void setType(final AccountGroup.Type newType) {
final boolean system = newType == AccountGroup.Type.SYSTEM;
typeSystem.setVisible(system);
typeSelect.setVisible(!system);
saveType.setVisible(!system);
- externalPanel.setVisible(newType == AccountGroup.Type.LDAP);
- externalNameFilter.setText(groupNameTxt.getText());
if (!system) {
for (int i = 0; i < typeSelect.getItemCount(); i++) {
@@ -367,75 +305,6 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
});
}
- private void doExternalSearch() {
- externalNameFilter.setEnabled(false);
- externalNameSearch.setEnabled(false);
- Util.GROUP_SVC.searchExternalGroups(externalNameFilter.getText(),
- new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
- @Override
- public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
- final CellFormatter fmt = externalMatches.getCellFormatter();
-
- if (result.isEmpty()) {
- externalMatches.resize(1, 1);
- externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
- fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
- return;
- }
-
- externalMatches.resize(1 + result.size(), 2);
-
- externalMatches.setText(0, 0, Util.C.columnGroupName());
- externalMatches.setText(0, 1, "");
- fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
- fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
-
- for (int row = 0; row < result.size(); row++) {
- final AccountGroup.ExternalNameKey key = result.get(row);
- final Button b = new Button(Util.C.buttonSelectGroup());
- b.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(ClickEvent event) {
- setExternalGroup(key);
- }
- });
- externalMatches.setText(1 + row, 0, key.get());
- externalMatches.setWidget(1 + row, 1, b);
- fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
- }
- externalMatches.setVisible(true);
-
- externalNameFilter.setEnabled(true);
- externalNameSearch.setEnabled(true);
- }
-
- @Override
- public void onFailure(Throwable caught) {
- externalNameFilter.setEnabled(true);
- externalNameSearch.setEnabled(true);
- super.onFailure(caught);
- }
- });
- }
-
- private void setExternalGroup(final AccountGroup.ExternalNameKey key) {
- externalMatches.setVisible(false);
-
- Util.GROUP_SVC.changeExternalGroup(getGroupId(), key,
- new GerritCallback<VoidResult>() {
- @Override
- public void onSuccess(VoidResult result) {
- externalName.setText(key.get());
- }
-
- @Override
- public void onFailure(Throwable caught) {
- externalMatches.setVisible(true);
- super.onFailure(caught);
- }
- });
- }
-
@Override
protected void display(final GroupDetail groupDetail) {
final AccountGroup group = groupDetail.group;
@@ -444,19 +313,12 @@ public class AccountGroupInfoScreen extends AccountGroupScreen {
if (groupDetail.ownerGroup != null) {
ownerTxt.setText(groupDetail.ownerGroup.getName());
} else {
- ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
+ ownerTxt.setText(Util.M.deletedReference(group.getOwnerGroupUUID().get()));
}
descTxt.setText(group.getDescription());
visibleToAllCheckBox.setValue(group.isVisibleToAll());
- switch (group.getType()) {
- case LDAP:
- externalName.setText(group.getExternalNameKey() != null ? group
- .getExternalNameKey().get() : Util.C.noGroupSelected());
- break;
- }
-
setType(group.getType());
enableForm(groupDetail.canModify);
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 b5cca8627e..4c0b1ba16c 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
@@ -17,8 +17,8 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
@@ -286,7 +286,7 @@ public class AccountGroupMembersScreen extends AccountGroupScreen {
CheckBox checkBox = new CheckBox();
table.setWidget(row, 1, checkBox);
checkBox.setEnabled(enabled);
- table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
+ table.setWidget(row, 2, AccountLink.link(accounts, accountId));
table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 9250ca345e..0c18169a74 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -48,6 +48,9 @@ public interface AdminConstants extends Constants {
String suggestedGroupLabel();
String parentSuggestions();
+ String buttonBrowseProjects();
+ String projects();
+ String projectRepoBrowser();
String headingGroupUUID();
String headingOwner();
String headingDescription();
@@ -58,7 +61,6 @@ public interface AdminConstants extends Constants {
String noMembersInfo();
String headingExternalGroup();
String headingCreateGroup();
- String headingCreateProject();
String headingParentProjectName();
String columnProjectName();
String headingAgreements();
@@ -89,12 +91,14 @@ public interface AdminConstants extends Constants {
String initialRevision();
String buttonAddBranch();
String buttonDeleteBranch();
+ String branchDeletionOpenChanges();
String groupListPrev();
String groupListNext();
String groupListOpen();
String groupListTitle();
+ String createGroupTitle();
String groupTabGeneral();
String groupTabMembers();
String projectListTitle();
@@ -103,6 +107,13 @@ public interface AdminConstants extends Constants {
String projectAdminTabBranches();
String projectAdminTabAccess();
+ String plugins();
+ String pluginDisabled();
+
+ String columnPluginName();
+ String columnPluginVersion();
+ String columnPluginStatus();
+
String noGroupSelected();
String errorNoMatchingGroups();
String errorNoGitRepository();
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 ef7b73dd9b..3ff3f43b4d 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
@@ -17,6 +17,9 @@ buttonSelectGroup = Select
buttonSaveChanges = Save Changes
checkBoxEmptyCommit = Create initial empty commit
checkBoxPermissionsOnly = Only serve as parent for other projects
+buttonBrowseProjects = Browse
+projects = All projects
+projectRepoBrowser = Repository Browser
useContentMerge = Automatically resolve conflicts
useContributorAgreements = Require a valid contributor agreement to upload
useSignedOffBy = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-signedoffby.html#Signed-off-by" target="_blank"><code>Signed-off-by</code></a> in commit message
@@ -39,7 +42,6 @@ headingIncludedGroups = Included Groups
noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For external groups and Gerrit system groups the members cannot be displayed.
headingExternalGroup = Selected External Group
headingCreateGroup = Create New Group
-headingCreateProject = Create New Project
headingAgreements = Contributor Agreements
projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
@@ -68,12 +70,15 @@ columnBranchRevision = Revision
initialRevision = Initial Revision
buttonAddBranch = Create Branch
buttonDeleteBranch = Delete
+branchDeletionOpenChanges = The following branches were not deleted \
+because they have open changes:
groupListPrev = Previous group
groupListNext = Next group
groupListOpen = Open group
groupListTitle = Groups
+createGroupTitle = Create Group
groupTabGeneral = General
groupTabMembers = Members
projectListTitle = Projects
@@ -82,6 +87,12 @@ projectAdminTabGeneral = General
projectAdminTabBranches = Branches
projectAdminTabAccess = Access
+plugins = Plugins
+pluginDisabled = Disabled
+columnPluginName = Plugin Name
+columnPluginVersion = Version
+columnPluginStatus = Status
+
noGroupSelected = (No group selected)
errorNoMatchingGroups = No Matching Groups
errorNoGitRepository = No Git Repository
@@ -91,6 +102,7 @@ addPermission = Add Permission ...
# Permission Names
permissionNames = \
+ abandon, \
create, \
forgeAuthor, \
forgeCommitter, \
@@ -102,6 +114,7 @@ permissionNames = \
read, \
rebase, \
submit
+abandon = Abandon
create = Create Reference
forgeAuthor = Forge Author Identity
forgeCommitter = Forge Committer Identity
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
new file mode 100644
index 0000000000..4574c6d766
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -0,0 +1,121 @@
+// 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.client.admin;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+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.user.client.History;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+public class CreateGroupScreen extends Screen {
+
+ private NpTextBox addTxt;
+ private Button addNew;
+
+ public CreateGroupScreen() {
+ super();
+ setRequiresSignIn(true);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities ac) {
+ if (ac.canPerform(CREATE_GROUP)) {
+ display();
+ } else {
+ Gerrit.display(PageLinks.ADMIN_CREATE_GROUP, new NotFoundScreen());
+ }
+ }
+ }, CREATE_GROUP);
+ }
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+ setPageTitle(Util.C.createGroupTitle());
+ addCreateGroupPanel();
+ }
+
+ private void addCreateGroupPanel() {
+ VerticalPanel addPanel = new VerticalPanel();
+ addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
+ addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
+
+ addTxt = new NpTextBox();
+ addTxt.setVisibleLength(60);
+ addTxt.addKeyPressHandler(new KeyPressHandler() {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+ doCreateGroup();
+ }
+ }
+ });
+ addPanel.add(addTxt);
+
+ addNew = new Button(Util.C.buttonCreateGroup());
+ addNew.setEnabled(false);
+ addNew.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ doCreateGroup();
+ }
+ });
+ addPanel.add(addNew);
+ add(addPanel);
+
+ new OnEditEnabler(addNew, addTxt);
+ }
+
+ private void doCreateGroup() {
+ final String newName = addTxt.getText();
+ if (newName == null || newName.length() == 0) {
+ return;
+ }
+
+ addNew.setEnabled(false);
+ Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
+ public void onSuccess(final AccountGroup.Id result) {
+ History.newItem(Dispatcher.toGroup(result, AccountGroupScreen.MEMBERS));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ super.onFailure(caught);
+ addNew.setEnabled(true);
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 0ce2d775a7..b4950d80b0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -14,14 +14,22 @@
package com.google.gerrit.client.admin;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.ProjectListPopup;
import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -38,16 +46,17 @@ import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.List;
-
public class CreateProjectScreen extends Screen {
+ private Grid grid;
private NpTextBox project;
private Button create;
+ private Button browse;
private HintTextBox parent;
private SuggestBox sugestParent;
private CheckBox emptyCommit;
private CheckBox permissionsOnly;
private ProjectsTable suggestedParentsTab;
+ private ProjectListPopup projectsPopup;
public CreateProjectScreen() {
super();
@@ -57,7 +66,22 @@ public class CreateProjectScreen extends Screen {
@Override
protected void onLoad() {
super.onLoad();
- display();
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities ac) {
+ if (ac.canPerform(CREATE_PROJECT)) {
+ display();
+ } else {
+ Gerrit.display(PageLinks.ADMIN_CREATE_PROJECT, new NotFoundScreen());
+ }
+ }
+ }, CREATE_PROJECT);
+ }
+
+ @Override
+ protected void onUnload() {
+ super.onUnload();
+ projectsPopup.closePopup();
}
@Override
@@ -65,6 +89,18 @@ public class CreateProjectScreen extends Screen {
super.onInitUI();
setPageTitle(Util.C.createProjectTitle());
addCreateProjectPanel();
+
+ /* popup */
+ projectsPopup = new ProjectListPopup() {
+ @Override
+ protected void onMovePointerTo(String projectName) {
+ // prevent user input from being overwritten by simply poping up
+ if (!projectsPopup.isPopingUp() || "".equals(sugestParent.getText())) {
+ sugestParent.setText(projectName);
+ }
+ }
+ };
+ projectsPopup.initPopup(Util.C.projects(), PageLinks.ADMIN_PROJECTS);
}
private void addCreateProjectPanel() {
@@ -82,12 +118,11 @@ public class CreateProjectScreen extends Screen {
fp.add(emptyCommit);
fp.add(permissionsOnly);
fp.add(create);
- VerticalPanel vp=new VerticalPanel();
+ VerticalPanel vp = new VerticalPanel();
vp.add(fp);
initSuggestedParents();
vp.add(suggestedParentsTab);
add(vp);
-
}
private void initCreateTxt() {
@@ -111,6 +146,23 @@ public class CreateProjectScreen extends Screen {
doCreateProject();
}
});
+
+ browse = new Button(Util.C.buttonBrowseProjects());
+ browse.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ int top = grid.getAbsoluteTop() - 50; // under page header
+ // Try to place it to the right of everything else, but not
+ // right justified
+ int left =
+ 5 + Math.max(
+ grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+ suggestedParentsTab.getAbsoluteLeft()
+ + suggestedParentsTab.getOffsetWidth());
+ projectsPopup.setPreferredCoordinates(top, left);
+ projectsPopup.displayPopup();
+ }
+ });
}
private void initParentBox() {
@@ -127,45 +179,44 @@ public class CreateProjectScreen extends Screen {
}
@Override
- protected void populate(final int row, final Project k) {
- final Anchor projectLink = new Anchor(k.getName());
+ protected void populate(final int row, final ProjectInfo k) {
+ final Anchor projectLink = new Anchor(k.name());
projectLink.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
- sugestParent.setText(getRowItem(row).getName());
+ sugestParent.setText(getRowItem(row).name());
}
});
table.setWidget(row, 1, projectLink);
- table.setText(row, 2, k.getDescription());
+ table.setText(row, 2, k.description());
setRowItem(row, k);
}
};
suggestedParentsTab.setVisible(false);
- Util.PROJECT_SVC
- .suggestParentCandidates(new GerritCallback<List<Project>>() {
- @Override
- public void onSuccess(List<Project> result) {
- if (result != null && !result.isEmpty()) {
- suggestedParentsTab.setVisible(true);
- suggestedParentsTab.display(result);
- suggestedParentsTab.finishDisplay();
- }
- }
- });
+ ProjectMap.parentCandidates(new GerritCallback<ProjectMap>() {
+ @Override
+ public void onSuccess(ProjectMap list) {
+ if (!list.isEmpty()) {
+ suggestedParentsTab.setVisible(true);
+ suggestedParentsTab.display(list);
+ suggestedParentsTab.finishDisplay();
+ }
+ }
+ });
}
private void addGrid(final VerticalPanel fp) {
- final Grid grid = new Grid(2, 2);
+ grid = new Grid(2, 3);
grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
grid.setText(0, 0, Util.C.columnProjectName() + ":");
grid.setWidget(0, 1, project);
grid.setText(1, 0, Util.C.headingParentProjectName() + ":");
grid.setWidget(1, 1, sugestParent);
-
+ grid.setWidget(1, 2, browse);
fp.add(grid);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 9d62d34baa..3157d9777d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,37 +14,14 @@
package com.google.gerrit.client.admin;
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-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.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-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.user.client.History;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
public class GroupListScreen extends AccountScreen {
private GroupTable groups;
- private VerticalPanel addPanel;
- private NpTextBox addTxt;
- private Button addNew;
-
@Override
protected void onLoad() {
super.onLoad();
@@ -52,7 +29,6 @@ public class GroupListScreen extends AccountScreen {
.visibleGroups(new ScreenLoadCallback<GroupList>(this) {
@Override
protected void preDisplay(GroupList result) {
- addPanel.setVisible(result.isCanCreateGroup());
groups.display(result.getGroups());
groups.finishDisplay();
}
@@ -66,54 +42,6 @@ public class GroupListScreen extends AccountScreen {
groups = new GroupTable(true /* hyperlink to admin */, PageLinks.ADMIN_GROUPS);
add(groups);
-
- addPanel = new VerticalPanel();
- addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
- addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
-
- addTxt = new NpTextBox();
- addTxt.setVisibleLength(60);
- addTxt.addKeyPressHandler(new KeyPressHandler() {
- @Override
- public void onKeyPress(KeyPressEvent event) {
- if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
- doCreateGroup();
- }
- }
- });
- addPanel.add(addTxt);
-
- addNew = new Button(Util.C.buttonCreateGroup());
- addNew.setEnabled(false);
- addNew.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(final ClickEvent event) {
- doCreateGroup();
- }
- });
- addNew.addFocusHandler(new FocusHandler() {
- @Override
- public void onFocus(FocusEvent event) {
- // unregister the keys for the 'groups' table so that pressing ENTER
- // when the 'addNew' button has the focus triggers the button (if the
- // keys for the 'groups' table would not be unregistered the 'addNew'
- // button would not be triggered on ENTER but the group which is
- // selected in the 'groups' table would be opened)
- groups.setRegisterKeys(false);
- }
- });
- addNew.addBlurHandler(new BlurHandler() {
- @Override
- public void onBlur(BlurEvent event) {
- // re-register the keys for the 'groups' table when the 'addNew' button
- // gets blurred
- groups.setRegisterKeys(true);
- }
- });
- addPanel.add(addNew);
- add(addPanel);
-
- new OnEditEnabler(addNew, addTxt);
}
@Override
@@ -121,24 +49,4 @@ public class GroupListScreen extends AccountScreen {
super.registerKeys();
groups.setRegisterKeys(true);
}
-
- private void doCreateGroup() {
- final String newName = addTxt.getText();
- if (newName == null || newName.length() == 0) {
- return;
- }
-
- addNew.setEnabled(false);
- Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
- public void onSuccess(final AccountGroup.Id result) {
- History.newItem(Dispatcher.toGroup(result));
- }
-
- @Override
- public void onFailure(Throwable caught) {
- super.onFailure(caught);
- addNew.setEnabled(true);
- }
- });
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index 9da9c22cb6..4ddc9a835e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -17,6 +17,7 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
import com.google.gerrit.client.ui.RPCSuggestOracle;
import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.editor.client.LeafValueEditor;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -140,4 +141,8 @@ public class GroupReferenceBox extends Composite implements
public void setAccessKey(char key) {
suggestBox.setAccessKey(key);
}
+
+ public void setProject(Project.NameKey projectName) {
+ oracle.setProject(projectName);
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 7f7322627c..68188413ab 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -18,7 +18,6 @@ import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -32,7 +31,7 @@ import java.util.List;
public class GroupTable extends NavigationTable<AccountGroup> {
- private static final int NUM_COLS = 5;
+ private static final int NUM_COLS = 3;
private final boolean enableLink;
@@ -52,9 +51,7 @@ public class GroupTable extends NavigationTable<AccountGroup> {
table.setText(0, 1, Util.C.columnGroupName());
table.setText(0, 2, Util.C.columnGroupDescription());
- table.setText(0, 3, Util.C.headingOwner());
- table.setText(0, 4, Util.C.columnGroupType());
- table.setText(0, 5, Util.C.columnGroupVisibleToAll());
+ table.setText(0, 3, Util.C.columnGroupVisibleToAll());
table.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
@@ -82,20 +79,19 @@ public class GroupTable extends NavigationTable<AccountGroup> {
History.newItem(Dispatcher.toGroup(getRowItem(row).getId()));
}
- public void display(final List<GroupDetail> result) {
+ public void display(final List<AccountGroup> result) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for(GroupDetail detail : result) {
+ for(AccountGroup group : result) {
final int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
- populate(row, detail);
+ populate(row, group);
}
}
- void populate(final int row, final GroupDetail detail) {
- AccountGroup k = detail.group;
+ void populate(final int row, final AccountGroup k) {
if (enableLink) {
table.setWidget(row, 1, new Hyperlink(k.getName(),
Dispatcher.toGroup(k.getId())));
@@ -103,10 +99,8 @@ public class GroupTable extends NavigationTable<AccountGroup> {
table.setText(row, 1, k.getName());
}
table.setText(row, 2, k.getDescription());
- table.setText(row, 3, detail.ownerGroup.getName());
- table.setText(row, 4, k.getType().toString());
if (k.isVisibleToAll()) {
- table.setWidget(row, 5, new Image(Gerrit.RESOURCES.greenCheck()));
+ table.setWidget(row, 3, new Image(Gerrit.RESOURCES.greenCheck()));
}
final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index d10afd124d..2c43233275 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -25,6 +25,7 @@ import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -98,20 +99,25 @@ public class PermissionEditor extends Composite implements Editor<Permission>,
@UiField
DivElement deleted;
+ private final Project.NameKey projectName;
private final boolean readOnly;
private final AccessSection section;
private Permission value;
private PermissionRange.WithDefaults validRange;
private boolean isDeleted;
- public PermissionEditor(boolean readOnly, AccessSection section) {
+ public PermissionEditor(Project.NameKey projectName,
+ boolean readOnly,
+ AccessSection section) {
this.readOnly = readOnly;
this.section = section;
+ this.projectName = projectName;
normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
initWidget(uiBinder.createAndBindUi(this));
+ groupToAdd.setProject(projectName);
rules = ListEditor.of(new RuleEditorSource());
exclusiveGroup.setEnabled(!readOnly);
@@ -223,7 +229,8 @@ public class PermissionEditor extends Composite implements Editor<Permission>,
// If the oracle didn't get to complete a UUID, resolve it now.
//
addRule.setEnabled(false);
- SuggestUtil.SVC.suggestAccountGroup(ref.getName(), 1,
+ SuggestUtil.SVC.suggestAccountGroupForProject(
+ projectName, ref.getName(), 1,
new GerritCallback<List<GroupReference>>() {
@Override
public void onSuccess(List<GroupReference> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index e4cced7aa2..5dd8b3c536 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -25,6 +25,7 @@ import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.SpanElement;
@@ -178,16 +179,21 @@ public class PermissionRuleEditor extends Composite implements
@Override
public void setValue(PermissionRule value) {
GroupReference ref = value.getGroup();
- if (ref.getUUID() != null) {
+
+ boolean link;
+ if (ref.getUUID() != null && AccountGroup.isInternalGroup(ref.getUUID())) {
+ groupNameLink.setText(ref.getName());
groupNameLink.setTargetHistoryToken(Dispatcher.toGroup(ref.getUUID()));
+ link = true;
+ } else {
+ groupNameSpan.setInnerText(ref.getName());
+ groupNameSpan.setTitle(ref.getUUID() != null ? ref.getUUID().get() : "");
+ link = false;
}
- groupNameLink.setText(ref.getName());
- groupNameSpan.setInnerText(ref.getName());
deletedGroupName.setInnerText(ref.getName());
-
- groupNameLink.setVisible(ref.getUUID() != null);
- UIObject.setVisible(groupNameSpan, ref.getUUID() == null);
+ groupNameLink.setVisible(link);
+ UIObject.setVisible(groupNameSpan, !link);
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
new file mode 100644
index 0000000000..3948b3522e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -0,0 +1,107 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.plugins.PluginInfo;
+import com.google.gerrit.client.plugins.PluginMap;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Panel;
+
+public class PluginListScreen extends PluginScreen {
+
+ private Panel pluginPanel;
+ private PluginTable pluginTable;
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+ initPluginList();
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ PluginMap.all(new ScreenLoadCallback<PluginMap>(this) {
+ @Override
+ protected void preDisplay(final PluginMap result) {
+ pluginTable.display(result);
+ }
+ });
+ }
+
+ private void initPluginList() {
+ pluginTable = new PluginTable();
+ pluginTable.addStyleName(Gerrit.RESOURCES.css().pluginsTable());
+
+ pluginPanel = new FlowPanel();
+ pluginPanel.setWidth("500px");
+ pluginPanel.add(pluginTable);
+ add(pluginPanel);
+ }
+
+ private class PluginTable extends FancyFlexTable<PluginInfo> {
+ PluginTable() {
+ table.setText(0, 1, Util.C.columnPluginName());
+ table.setText(0, 2, Util.C.columnPluginVersion());
+ table.setText(0, 3, Util.C.columnPluginStatus());
+
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+ fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+ fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+ }
+
+ void display(final PluginMap plugins) {
+ while (1 < table.getRowCount()) {
+ table.removeRow(table.getRowCount() - 1);
+ }
+
+ for (final PluginInfo p : plugins.values().asList()) {
+ final int row = table.getRowCount();
+ table.insertRow(row);
+ applyDataRowStyle(row);
+ populate(row, p);
+ }
+ }
+
+ void populate(final int row, final PluginInfo plugin) {
+ if (plugin.isDisabled()) {
+ table.setText(row, 1, plugin.name());
+ } else {
+ table.setWidget(
+ row,
+ 1,
+ new Anchor(plugin.name(), Gerrit.selfRedirect("/plugins/"
+ + plugin.name() + "/")));
+ }
+ table.setText(row, 2, plugin.version());
+ if (plugin.isDisabled()) {
+ table.setText(row, 3, Util.C.pluginDisabled());
+ }
+
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+ fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+ fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+ setRowItem(row, plugin);
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
new file mode 100644
index 0000000000..dabcb4505f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
@@ -0,0 +1,31 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.ui.Screen;
+
+public abstract class PluginScreen extends Screen {
+
+ public PluginScreen() {
+ setRequiresSignIn(true);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+ setPageTitle(Util.C.plugins());
+ display();
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index e3bf555b09..32bc46974c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -120,7 +120,7 @@ public class ProjectAccessEditor extends Composite implements
history.getStyle().setDisplay(Display.NONE);
}
- addSection.setVisible(value != null && editing && !value.getOwnerOf().isEmpty());
+ addSection.setVisible(value != null && editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 1ed919b401..4403ce6302 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,7 +18,9 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
@@ -30,9 +32,15 @@ import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.globalkey.client.NpTextArea;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
public class ProjectAccessScreen extends ProjectScreen {
interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
}
@@ -57,6 +65,9 @@ public class ProjectAccessScreen extends ProjectScreen {
Button cancel2;
@UiField
+ VerticalPanel error;
+
+ @UiField
ProjectAccessEditor accessEditor;
@UiField
@@ -68,6 +79,9 @@ public class ProjectAccessScreen extends ProjectScreen {
@UiField
Button commit;
+ @UiField
+ Button review;
+
private Driver driver;
private ProjectAccess access;
@@ -101,8 +115,8 @@ public class ProjectAccessScreen extends ProjectScreen {
private void displayReadOnly(ProjectAccess access) {
this.access = access;
accessEditor.setEditing(false);
- UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty());
- edit.setEnabled(!access.getOwnerOf().isEmpty());
+ UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty() || access.canUpload());
+ edit.setEnabled(!access.getOwnerOf().isEmpty() || access.canUpload());
cancel1.setVisible(false);
UIObject.setVisible(commitTools, false);
driver.edit(access);
@@ -110,13 +124,29 @@ public class ProjectAccessScreen extends ProjectScreen {
@UiHandler("edit")
void onEdit(ClickEvent event) {
+ resetEditors();
+
edit.setEnabled(false);
cancel1.setVisible(true);
UIObject.setVisible(commitTools, true);
+ commit.setVisible(!access.getOwnerOf().isEmpty());
+ review.setVisible(access.canUpload());
accessEditor.setEditing(true);
driver.edit(access);
}
+ private void resetEditors() {
+ // Push an empty instance through the driver before pushing the real
+ // data. This will force GWT to delete and recreate the editors, which
+ // is required to build initialize them as editable vs. read-only.
+ ProjectAccess mock = new ProjectAccess();
+ mock.setProjectName(access.getProjectName());
+ mock.setRevision(access.getRevision());
+ mock.setLocal(Collections.<AccessSection> emptyList());
+ mock.setOwnerOf(Collections.<String> emptySet());
+ driver.edit(mock);
+ }
+
@UiHandler(value={"cancel1", "cancel2"})
void onCancel(ClickEvent event) {
Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
@@ -124,7 +154,7 @@ public class ProjectAccessScreen extends ProjectScreen {
@UiHandler("commit")
void onCommit(ClickEvent event) {
- ProjectAccess access = driver.flush();
+ final ProjectAccess access = driver.flush();
if (driver.hasErrors()) {
Window.alert(Util.C.errorsMustBeFixed());
@@ -144,14 +174,88 @@ public class ProjectAccessScreen extends ProjectScreen {
access.getLocal(), //
new GerritCallback<ProjectAccess>() {
@Override
- public void onSuccess(ProjectAccess access) {
+ public void onSuccess(ProjectAccess newAccess) {
enable(true);
commitMessage.setText("");
- displayReadOnly(access);
+ error.clear();
+ final Set<String> diffs = getDiffs(access, newAccess);
+ if (diffs.isEmpty()) {
+ displayReadOnly(newAccess);
+ } else {
+ error.add(new Label(Gerrit.C.projectAccessError()));
+ for (final String diff : diffs) {
+ error.add(new Label(diff));
+ }
+ if (access.canUpload()) {
+ error.add(new Label(Gerrit.C.projectAccessProposeForReviewHint()));
+ }
+ }
+ }
+
+ private Set<String> getDiffs(ProjectAccess wantedAccess,
+ ProjectAccess newAccess) {
+ final HashSet<AccessSection> same =
+ new HashSet<AccessSection>(wantedAccess.getLocal());
+ final HashSet<AccessSection> different =
+ new HashSet<AccessSection>(wantedAccess.getLocal().size()
+ + newAccess.getLocal().size());
+ different.addAll(wantedAccess.getLocal());
+ different.addAll(newAccess.getLocal());
+ same.retainAll(newAccess.getLocal());
+ different.removeAll(same);
+
+ final Set<String> differentNames = new HashSet<String>();
+ for (final AccessSection s : different) {
+ differentNames.add(s.getName());
+ }
+ return differentNames;
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ error.clear();
+ enable(true);
+ super.onFailure(caught);
+ }
+ });
+ }
+
+ @UiHandler("review")
+ void onReview(ClickEvent event) {
+ final ProjectAccess access = driver.flush();
+
+ if (driver.hasErrors()) {
+ Window.alert(Util.C.errorsMustBeFixed());
+ return;
+ }
+
+ String message = commitMessage.getText().trim();
+ if ("".equals(message)) {
+ message = null;
+ }
+
+ enable(false);
+ Util.PROJECT_SVC.reviewProjectAccess( //
+ getProjectKey(), //
+ access.getRevision(), //
+ message, //
+ access.getLocal(), //
+ new GerritCallback<Change.Id>() {
+ @Override
+ public void onSuccess(Change.Id changeId) {
+ enable(true);
+ commitMessage.setText("");
+ error.clear();
+ if (changeId != null) {
+ Gerrit.display(PageLinks.toChange(changeId));
+ } else {
+ displayReadOnly(access);
+ }
}
@Override
public void onFailure(Throwable caught) {
+ error.clear();
enable(true);
super.onFailure(caught);
}
@@ -160,7 +264,8 @@ public class ProjectAccessScreen extends ProjectScreen {
private void enable(boolean enabled) {
commitMessage.setEnabled(enabled);
- commit.setEnabled(enabled);
+ commit.setEnabled(enabled ? !access.getOwnerOf().isEmpty() : false);
+ review.setEnabled(enabled ? access.canUpload() : false);
cancel1.setEnabled(enabled);
cancel2.setEnabled(enabled);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
index 25361599eb..a664191daa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -32,6 +32,11 @@ limitations under the License.
.commitMessage .gwt-TextArea {
margin: 5px 5px 5px 5px;
}
+ .errorMessage {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ color: red;
+ }
</ui:style>
<g:HTMLPanel>
@@ -58,12 +63,21 @@ limitations under the License.
spellCheck='true'
/>
</div>
+ <g:VerticalPanel
+ styleName='{style.errorMessage}'
+ ui:field='error'>
+ </g:VerticalPanel>
<g:Button
ui:field='commit'
text='Save Changes'>
<ui:attribute name='text'/>
</g:Button>
<g:Button
+ ui:field='review'
+ text='Save for Review'>
+ <ui:attribute name='text'/>
+ </g:Button>
+ <g:Button
ui:field='cancel2'
text='Cancel'>
<ui:attribute name='text'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index f3ecbb3719..56d9417ea7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -16,16 +16,19 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.ConfirmationCallback;
import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.HintTextBox;
import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.InvalidRevisionException;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -41,6 +44,7 @@ import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -260,24 +264,52 @@ public class ProjectBranchesScreen extends ProjectScreen {
b.toSafeHtml(), new ConfirmationCallback() {
@Override
public void onOk() {
- Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
- new GerritCallback<Set<Branch.NameKey>>() {
- public void onSuccess(final Set<Branch.NameKey> deleted) {
- for (int row = 1; row < table.getRowCount();) {
- final Branch k = getRowItem(row);
- if (k != null && deleted.contains(k.getNameKey())) {
- table.removeRow(row);
- } else {
- row++;
- }
- }
- }
- });
+ deleteBranches(ids);
}
});
confirmationDialog.center();
}
+ private void deleteBranches(final Set<Branch.NameKey> branchIds) {
+ Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
+ new GerritCallback<Set<Branch.NameKey>>() {
+ public void onSuccess(final Set<Branch.NameKey> deleted) {
+ if (!deleted.isEmpty()) {
+ for (int row = 1; row < table.getRowCount();) {
+ final Branch k = getRowItem(row);
+ if (k != null && deleted.contains(k.getNameKey())) {
+ table.removeRow(row);
+ } else {
+ row++;
+ }
+ }
+ }
+
+ branchIds.removeAll(deleted);
+ if (!branchIds.isEmpty()) {
+ final VerticalPanel p = new VerticalPanel();
+ final ErrorDialog errorDialog = new ErrorDialog(p);
+ final Label l = new Label(Util.C.branchDeletionOpenChanges());
+ l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
+ p.add(l);
+ for (final Branch.NameKey branch : branchIds) {
+ final BranchLink link =
+ new BranchLink(branch.getParentKey(), Change.Status.NEW,
+ branch.get(), null) {
+ @Override
+ public void go() {
+ errorDialog.hide();
+ super.go();
+ };
+ };
+ p.add(link);
+ }
+ errorDialog.center();
+ }
+ }
+ });
+ }
+
void display(final List<Branch> result) {
canDelete = false;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 3599c3c032..91256220bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -16,28 +16,27 @@ package com.google.gerrit.client.admin;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Anchor;
public class ProjectListScreen extends Screen {
- private VerticalPanel createProjectLinkPanel;
private ProjectsTable projects;
@Override
protected void onLoad() {
super.onLoad();
- Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<ProjectList>(this) {
+ ProjectMap.all(new ScreenLoadCallback<ProjectMap>(this) {
@Override
- protected void preDisplay(final ProjectList result) {
- createProjectLinkPanel.setVisible(result.canCreateProject());
- projects.display(result.getProjects());
+ protected void preDisplay(final ProjectMap result) {
+ projects.display(result);
projects.finishDisplay();
}
});
@@ -48,27 +47,44 @@ public class ProjectListScreen extends Screen {
super.onInitUI();
setPageTitle(Util.C.projectListTitle());
- createProjectLinkPanel = new VerticalPanel();
- createProjectLinkPanel.setStyleName(Gerrit.RESOURCES.css()
- .createProjectLink());
- createProjectLinkPanel.add(new Hyperlink(Util.C.headingCreateProject(),
- PageLinks.ADMIN_CREATE_PROJECT));
- add(createProjectLinkPanel);
-
projects = new ProjectsTable() {
@Override
+ protected void initColumnHeaders() {
+ super.initColumnHeaders();
+ if (Gerrit.getGitwebLink() != null) {
+ table.setText(0, 3, Util.C.projectRepoBrowser());
+ table.getFlexCellFormatter().
+ addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+ }
+ }
+
+ @Override
protected void onOpenRow(final int row) {
History.newItem(link(getRowItem(row)));
}
- private String link(final Project item) {
- return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
+ private String link(final ProjectInfo item) {
+ return Dispatcher.toProjectAdmin(item.name_key(), ProjectScreen.INFO);
+ }
+
+ @Override
+ protected void insert(int row, ProjectInfo k) {
+ super.insert(row, k);
+ if (Gerrit.getGitwebLink() != null) {
+ table.getFlexCellFormatter().
+ addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+ }
}
@Override
- protected void populate(final int row, final Project k) {
- table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
- table.setText(row, 2, k.getDescription());
+ protected void populate(final int row, final ProjectInfo k) {
+ table.setWidget(row, 1, new Hyperlink(k.name(), link(k)));
+ table.setText(row, 2, k.description());
+ GitwebLink l = Gerrit.getGitwebLink();
+ if (l != null) {
+ table.setWidget(row, 3, new Anchor(l.getLinkName(), false, l.toProject(k
+ .name_key())));
+ }
setRowItem(row, k);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index ccfe2e613c..dd5b070bfd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -16,7 +16,6 @@ package com.google.gerrit.client.admin;
import static com.google.gerrit.client.Dispatcher.toProjectAdmin;
-import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.ui.MenuScreen;
import com.google.gerrit.reviewdb.client.Project;
@@ -30,12 +29,8 @@ public abstract class ProjectScreen extends MenuScreen {
public ProjectScreen(final Project.NameKey toShow) {
name = toShow;
- final boolean isWild = toShow.equals(Gerrit.getConfig().getWildProject());
-
link(Util.C.projectAdminTabGeneral(), toProjectAdmin(name, INFO));
- if (!isWild) {
- link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
- }
+ link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
link(Util.C.projectAdminTabAccess(), toProjectAdmin(name, ACCESS));
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java
new file mode 100644
index 0000000000..85dd794ee1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java
@@ -0,0 +1,68 @@
+// 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.client.auth.openid;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.auth.SignInMode;
+import com.google.gerrit.common.auth.openid.DiscoveryResult;
+import com.google.gwt.dom.client.FormElement;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.Hidden;
+
+import java.util.Map;
+
+public class OpenIdSsoPanel extends FlowPanel {
+ private final FormPanel redirectForm;
+ private final FlowPanel redirectBody;
+ private final String ssoUrl;
+
+ public OpenIdSsoPanel() {
+ super();
+ redirectBody = new FlowPanel();
+ redirectBody.setVisible(false);
+ redirectForm = new FormPanel();
+ redirectForm.add(redirectBody);
+
+ add(redirectForm);
+
+ ssoUrl = Gerrit.getConfig().getOpenIdSsoUrl();
+ }
+
+ public void authenticate(SignInMode requestedMode, final String token) {
+ OpenIdUtil.SVC.discover(ssoUrl, requestedMode, /* remember */ false, token,
+ new GerritCallback<DiscoveryResult>() {
+ public void onSuccess(final DiscoveryResult result) {
+ onDiscovery(result);
+ }
+ });
+ }
+
+ private void onDiscovery(final DiscoveryResult result) {
+ switch (result.status) {
+ case VALID:
+ redirectForm.setMethod(FormPanel.METHOD_POST);
+ redirectForm.setAction(result.providerUrl);
+ redirectBody.clear();
+ for (final Map.Entry<String, String> e : result.providerArgs.entrySet()) {
+ redirectBody.add(new Hidden(e.getKey(), e.getValue()));
+ }
+ FormElement.as(redirectForm.getElement()).setTarget("_top");
+ redirectForm.submit();
+ break;
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 1d881b6a30..05dd5d9b04 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -14,54 +14,65 @@
package com.google.gerrit.client.changes;
-import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.rpc.NativeList;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
+import java.util.Collections;
+import java.util.Comparator;
public class AccountDashboardScreen extends Screen implements ChangeListScreen {
private final Account.Id ownerId;
- private ChangeTable table;
- private ChangeTable.Section byOwner;
- private ChangeTable.Section forReview;
- private ChangeTable.Section closed;
+ private final boolean mine;
+ private ChangeTable2 table;
+ private ChangeTable2.Section outgoing;
+ private ChangeTable2.Section incoming;
+ private ChangeTable2.Section closed;
public AccountDashboardScreen(final Account.Id id) {
ownerId = id;
+ mine = Gerrit.isSignedIn() && ownerId.equals(Gerrit.getUserAccount().getId());
}
@Override
protected void onInitUI() {
super.onInitUI();
- table = new ChangeTable(true);
+ table = new ChangeTable2();
table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
- byOwner = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
- forReview = new ChangeTable.Section("", ApprovalViewType.USER, ownerId);
- closed = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
- table.addSection(byOwner);
- table.addSection(forReview);
+ outgoing = new ChangeTable2.Section();
+ incoming = new ChangeTable2.Section();
+ closed = new ChangeTable2.Section();
+
+ outgoing.setTitleText(Util.C.outgoingReviews());
+ incoming.setTitleText(Util.C.incomingReviews());
+ incoming.setHighlightUnreviewed(true);
+ closed.setTitleText(Util.C.recentlyClosed());
+
+ table.addSection(outgoing);
+ table.addSection(incoming);
table.addSection(closed);
add(table);
- table.setSavePointerId(PageLinks.toAccountDashboard(ownerId));
+ table.setSavePointerId("owner:" + ownerId);
}
@Override
protected void onLoad() {
super.onLoad();
- Util.LIST_SVC.forAccount(ownerId,
- new ScreenLoadCallback<AccountDashboardInfo>(this) {
+ String who = mine ? "self" : ownerId.toString();
+ ChangeList.query(
+ new ScreenLoadCallback<NativeList<ChangeList>>(this) {
@Override
- protected void preDisplay(final AccountDashboardInfo r) {
- display(r);
+ protected void preDisplay(NativeList<ChangeList> result) {
+ display(result);
}
- });
+ },
+ "is:open owner:" + who,
+ "is:open reviewer:" + who + " -owner:" + who,
+ "is:closed owner:" + who + " -age:1w limit:10");
}
@Override
@@ -70,20 +81,72 @@ public class AccountDashboardScreen extends Screen implements ChangeListScreen {
table.setRegisterKeys(true);
}
- private void display(final AccountDashboardInfo r) {
- table.setAccountInfoCache(r.getAccounts());
+ private void display(NativeList<ChangeList> result) {
+ if (!mine && !hasChanges(result)) {
+ // When no results are returned and the data is not for the
+ // current user, the target user is presumed to not exist.
+ Gerrit.display(getToken(), new NotFoundScreen());
+ return;
+ }
+
+ ChangeList out = result.get(0);
+ ChangeList in = result.get(1);
+ ChangeList done = result.get(2);
- final AccountInfo o = r.getAccounts().get(r.getOwner());
- final String name = FormatUtil.name(o);
- setWindowTitle(name);
- setPageTitle(Util.M.accountDashboardTitle(name));
- byOwner.setTitleText(Util.M.changesStartedBy(name));
- forReview.setTitleText(Util.M.changesReviewableBy(name));
- closed.setTitleText(Util.C.changesRecentlyClosed());
+ if (mine) {
+ setWindowTitle(Util.C.myDashboardTitle());
+ setPageTitle(Util.C.myDashboardTitle());
+ } else {
+ // The server doesn't tell us who the dashboard is for. Try to guess
+ // by looking at a change started by the owner and extract the name.
+ String name = guessName(out);
+ if (name == null) {
+ name = guessName(done);
+ }
+ if (name != null) {
+ setWindowTitle(name);
+ setPageTitle(Util.M.accountDashboardTitle(name));
+ } else {
+ setWindowTitle(Util.C.unknownDashboardTitle());
+ setWindowTitle(Util.C.unknownDashboardTitle());
+ }
+ }
- byOwner.display(r.getByOwner());
- forReview.display(r.getForReview());
- closed.display(r.getClosed());
+ Collections.sort(out.asList(), outComparator());
+
+ table.updateColumnsForLabels(out, in, done);
+ outgoing.display(out);
+ incoming.display(in);
+ closed.display(done);
table.finishDisplay();
}
+
+ private Comparator<ChangeInfo> outComparator() {
+ return new Comparator<ChangeInfo>() {
+ @Override
+ public int compare(ChangeInfo a, ChangeInfo b) {
+ int cmp = a.created().compareTo(b.created());
+ if (cmp != 0) return cmp;
+ return a._number() - b._number();
+ }
+ };
+ }
+
+ private boolean hasChanges(NativeList<ChangeList> result) {
+ for (ChangeList list : result.asList()) {
+ if (!list.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String guessName(ChangeList list) {
+ for (ChangeInfo change : list.asList()) {
+ if (change.owner() != null && change.owner().name() != null) {
+ return change.owner().name();
+ }
+ }
+ return null;
+ }
}
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 09716cc7cd..73036ffb16 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
@@ -21,7 +21,7 @@ import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.ReviewerSuggestOracle;
import com.google.gerrit.common.data.AccountInfoCache;
@@ -29,6 +29,7 @@ import com.google.gerrit.common.data.ApprovalDetail;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.PatchSetPublishDetail;
import com.google.gerrit.common.data.ReviewerResult;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
@@ -129,16 +130,26 @@ public class ApprovalTable extends Composite {
accountCache = aic;
}
- private AccountDashboardLink link(final Account.Id id) {
- return AccountDashboardLink.link(accountCache, id);
+ private AccountLink link(final Account.Id id) {
+ return AccountLink.link(accountCache, id);
+ }
+
+ void display(PatchSetPublishDetail detail) {
+ doDisplay(detail.getChange(), detail.getApprovals(),
+ detail.getSubmitRecords());
}
void display(ChangeDetail detail) {
- changeId = detail.getChange().getId();
+ doDisplay(detail.getChange(), detail.getApprovals(),
+ detail.getSubmitRecords());
+ }
+
+ private void doDisplay(Change change, List<ApprovalDetail> approvals,
+ List<SubmitRecord> submitRecords) {
+ changeId = change.getId();
reviewerSuggestOracle.setChange(changeId);
List<String> columns = new ArrayList<String>();
- List<ApprovalDetail> rows = detail.getApprovals();
final Element missingList = missing.getElement();
while (DOM.getChildCount(missingList) > 0) {
@@ -146,16 +157,16 @@ public class ApprovalTable extends Composite {
}
missing.setVisible(false);
- if (detail.getSubmitRecords() != null) {
+ if (submitRecords != null) {
HashSet<String> reportedMissing = new HashSet<String>();
HashMap<Account.Id, ApprovalDetail> byUser =
new HashMap<Account.Id, ApprovalDetail>();
- for (ApprovalDetail ad : detail.getApprovals()) {
+ for (ApprovalDetail ad : approvals) {
byUser.put(ad.getAccount(), ad);
}
- for (SubmitRecord rec : detail.getSubmitRecords()) {
+ for (SubmitRecord rec : submitRecords) {
if (rec.labels == null) {
continue;
}
@@ -182,6 +193,9 @@ public class ApprovalTable extends Composite {
break;
}
+ case MAY:
+ break;
+
case NEED:
case IMPOSSIBLE:
if (reportedMissing.add(lbl.label)) {
@@ -197,17 +211,19 @@ public class ApprovalTable extends Composite {
missing.setVisible(!reportedMissing.isEmpty());
} else {
- for (ApprovalDetail ad : rows) {
+ for (ApprovalDetail ad : approvals) {
for (PatchSetApproval psa : ad.getPatchSetApprovals()) {
ApprovalType legacyType = types.byId(psa.getCategoryId());
if (legacyType == null) {
continue;
}
String labelName = legacyType.getCategory().getLabelName();
- if (psa.getValue() == legacyType.getMax().getValue()) {
- ad.approved(labelName);
- } else if (psa.getValue() == legacyType.getMin().getValue()) {
- ad.rejected(labelName);
+ if (psa.getValue() != 0 ) {
+ if (psa.getValue() == legacyType.getMax().getValue()) {
+ ad.approved(labelName);
+ } else if (psa.getValue() == legacyType.getMin().getValue()) {
+ ad.rejected(labelName);
+ }
}
if (!columns.contains(labelName)) {
columns.add(labelName);
@@ -231,13 +247,13 @@ public class ApprovalTable extends Composite {
}
}
- if (rows.isEmpty()) {
+ if (approvals.isEmpty()) {
table.setVisible(false);
} else {
displayHeader(columns);
- table.resizeRows(1 + rows.size());
- for (int i = 0; i < rows.size(); i++) {
- displayRow(i + 1, rows.get(i), detail.getChange(), columns);
+ table.resizeRows(1 + approvals.size());
+ for (int i = 0; i < approvals.size(); i++) {
+ displayRow(i + 1, approvals.get(i), change, columns);
}
table.setVisible(true);
}
@@ -245,7 +261,7 @@ public class ApprovalTable extends Composite {
addReviewer.setVisible(Gerrit.isSignedIn());
if (Gerrit.getConfig().testChangeMerge()
- && !detail.getChange().isMergeable()) {
+ && !change.isMergeable()) {
Element li = DOM.createElement("li");
li.setClassName(Gerrit.RESOURCES.css().missingApproval());
DOM.setInnerText(li, Util.C.messageNeedsRebaseOrHasDependency());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
index 27f76f6e83..5a9da1a51d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
@@ -38,7 +38,6 @@ public class ChangeCache {
private Change.Id changeId;
private ChangeDetailCache detail;
private ListenableValue<ChangeInfo> info;
- private StarCache starred;
protected ChangeCache(Change.Id chg) {
changeId = chg;
@@ -61,11 +60,4 @@ public class ChangeCache {
}
return info;
}
-
- public StarCache getStarCache() {
- if (starred == null) {
- starred = new StarCache(changeId);
- }
- return starred;
- }
}
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 3372096a5c..d42992f63d 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
@@ -23,7 +23,11 @@ public interface ChangeConstants extends Constants {
String statusLongAbandoned();
String statusLongDraft();
- String changesRecentlyClosed();
+ String myDashboardTitle();
+ String unknownDashboardTitle();
+ String incomingReviews();
+ String outgoingReviews();
+ String recentlyClosed();
String starredHeading();
String watchedHeading();
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 ad7067408c..8ceb74caa9 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
@@ -7,7 +7,11 @@ statusLongDraft = Draft
starredHeading = Starred Changes
watchedHeading = Open Changes of Watched Projects
draftsHeading = Changes with unpublished drafts
-changesRecentlyClosed = Recently closed
+myDashboardTitle = My Reviews
+unknownDashboardTitle = Code Review Dashboard
+incomingReviews = Incoming reviews
+outgoingReviews = Outgoing reviews
+recentlyClosed = Recently closed
allOpenChanges = All open changes
allAbandonedChanges = All abandoned changes
allMergedChanges = All merged changes
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 92827091fc..c8b2a6658e 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
@@ -19,14 +19,15 @@ import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
public class ChangeDescriptionBlock extends Composite {
private final ChangeInfoBlock infoBlock;
private final CommitMessageBlock messageBlock;
- public ChangeDescriptionBlock() {
+ public ChangeDescriptionBlock(KeyCommandSet keysAction) {
infoBlock = new ChangeInfoBlock();
- messageBlock = new CommitMessageBlock();
+ messageBlock = new CommitMessageBlock(keysAction);
final HorizontalPanel hp = new HorizontalPanel();
hp.add(infoBlock);
@@ -34,9 +35,9 @@ public class ChangeDescriptionBlock extends Composite {
initWidget(hp);
}
- public void display(final Change chg, final PatchSetInfo info,
+ public void display(Change chg, Boolean starred, PatchSetInfo info,
final AccountInfoCache acc) {
infoBlock.display(chg, acc);
- messageBlock.display(info.getMessage());
+ messageBlock.display(chg.getId(), starred, info.getMessage());
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index bb28e11639..9c19d50ed3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -65,6 +65,7 @@ public class ChangeDetailCache extends ListenableValue<ChangeDetail> {
public static void setChangeDetail(ChangeDetail detail) {
Change.Id chgId = detail.getChange().getId();
ChangeCache.get(chgId).getChangeDetailCache().set(detail);
+ StarredChanges.fireChangeStarEvent(chgId, detail.isStarred());
}
private final Change.Id changeId;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
new file mode 100644
index 0000000000..0c8e03e022
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -0,0 +1,123 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+import java.util.Set;
+
+public class ChangeInfo extends JavaScriptObject {
+ public final Project.NameKey project_name_key() {
+ return new Project.NameKey(project());
+ }
+
+ public final Change.Id legacy_id() {
+ return new Change.Id(_number());
+ }
+
+ public final Timestamp created() {
+ Timestamp ts = _get_cts();
+ if (ts == null) {
+ ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
+ _set_cts(ts);
+ }
+ return ts;
+ }
+
+ private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
+ private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+
+ public final Timestamp updated() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ }
+
+ public final String id_abbreviated() {
+ return new Change.Key(id()).abbreviate();
+ }
+
+ public final Change.Status status() {
+ return Change.Status.valueOf(statusRaw());
+ }
+
+ public final Set<String> labels() {
+ return Natives.keys(labels0());
+ }
+
+ public final native String project() /*-{ return this.project; }-*/;
+ public final native String branch() /*-{ return this.branch; }-*/;
+ public final native String topic() /*-{ return this.topic; }-*/;
+ public final native String id() /*-{ return this.id; }-*/;
+ private final native String statusRaw() /*-{ return this.status; }-*/;
+ public final native String subject() /*-{ return this.subject; }-*/;
+ public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+ private final native String createdRaw() /*-{ return this.created; }-*/;
+ private final native String updatedRaw() /*-{ return this.updated; }-*/;
+ public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
+ public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
+ public final native String _sortkey() /*-{ return this._sortkey; }-*/;
+ private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
+ public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+ final native int _number() /*-{ return this._number; }-*/;
+ final native boolean _more_changes()
+ /*-{ return this._more_changes ? true : false; }-*/;
+
+ protected ChangeInfo() {
+ }
+
+ public static class AccountInfo extends JavaScriptObject {
+ public final native String name() /*-{ return this.name; }-*/;
+
+ protected AccountInfo() {
+ }
+ }
+
+ public static class LabelInfo extends JavaScriptObject {
+ public final SubmitRecord.Label.Status status() {
+ if (approved() != null) {
+ return SubmitRecord.Label.Status.OK;
+ } else if (rejected() != null) {
+ return SubmitRecord.Label.Status.REJECT;
+ } else if (optional()) {
+ return SubmitRecord.Label.Status.MAY;
+ } else {
+ return SubmitRecord.Label.Status.NEED;
+ }
+ }
+
+ public final native String name() /*-{ return this._name; }-*/;
+ public final native AccountInfo approved() /*-{ return this.approved; }-*/;
+ public final native AccountInfo rejected() /*-{ return this.rejected; }-*/;
+
+ public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
+ public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
+ public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
+ final native short _value()
+ /*-{
+ if (this.value) return this.value;
+ if (this.disliked) return -1;
+ if (this.recommended) return 1;
+ return 0;
+ }-*/;
+
+ protected LabelInfo() {
+ }
+ }
+}
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 f8373ccf8a..3ffacc3e57 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
@@ -17,15 +17,13 @@ package com.google.gerrit.client.changes;
import static com.google.gerrit.client.FormatUtil.mediumFormat;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.ProjectLink;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -40,18 +38,15 @@ public class ChangeInfoBlock extends Composite {
private static final int R_UPDATED = 6;
private static final int R_STATUS = 7;
private static final int R_MERGE_TEST = 8;
- private final int R_PERMALINK;
- private static final int R_CNT = 10;
+ private static final int R_CNT = 9;
private final Grid table;
public ChangeInfoBlock() {
if (Gerrit.getConfig().testChangeMerge()) {
table = new Grid(R_CNT, 2);
- R_PERMALINK = 9;
} else {
table = new Grid(R_CNT - 1, 2);
- R_PERMALINK = 8;
}
table.setStyleName(Gerrit.RESOURCES.css().infoBlock());
table.addStyleName(Gerrit.RESOURCES.css().changeInfoBlock());
@@ -73,8 +68,6 @@ public class ChangeInfoBlock extends Composite {
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(R_CHANGE_ID, 1, Gerrit.RESOURCES.css().changeid());
fmt.addStyleName(R_CNT - 2, 0, Gerrit.RESOURCES.css().bottomheader());
- fmt.addStyleName(R_PERMALINK, 0, Gerrit.RESOURCES.css().permalink());
- fmt.addStyleName(R_PERMALINK, 1, Gerrit.RESOURCES.css().permalink());
initWidget(table);
}
@@ -92,7 +85,7 @@ public class ChangeInfoBlock extends Composite {
changeIdLabel.setPreviewText(chg.getKey().get());
table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
- table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
+ table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
.getProject(), chg.getStatus(), dst.get(), null));
@@ -101,20 +94,21 @@ public class ChangeInfoBlock extends Composite {
table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));
+ final Change.Status status = chg.getStatus();
if (Gerrit.getConfig().testChangeMerge()) {
- table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
- .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
+ if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) {
+ table.getRowFormatter().setVisible(R_MERGE_TEST, true);
+ table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
+ .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
+ } else {
+ table.getRowFormatter().setVisible(R_MERGE_TEST, false);
+ }
}
- if (chg.getStatus().isClosed()) {
+ if (status.isClosed()) {
table.getCellFormatter().addStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
} else {
table.getCellFormatter().removeStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
}
-
- final FlowPanel fp = new FlowPanel();
- fp.add(new ChangeLink(Util.C.changePermalink(), chg.getId()));
- fp.add(new CopyableLabel(ChangeLink.permalink(chg.getId()), false));
- table.setWidget(R_PERMALINK, 1, fp);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
new file mode 100644
index 0000000000..560e6b38d7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -0,0 +1,86 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.util.EnumSet;
+
+/** List of changes available from {@code /changes/}. */
+public class ChangeList extends NativeList<ChangeInfo> {
+ private static final String URI = "/changes/";
+
+ /** Run 2 or more queries in a single remote invocation. */
+ public static void query(
+ AsyncCallback<NativeList<ChangeList>> callback, String... queries) {
+ assert queries.length >= 2; // At least 2 is required for correct result.
+ RestApi call = new RestApi(URI);
+ for (String q : queries) {
+ call.addParameterRaw("q", KeyUtil.encode(q));
+ }
+ addOptions(call, ListChangesOption.LABELS);
+ call.send(callback);
+ }
+
+ public static void prev(String query,
+ int limit, String sortkey,
+ AsyncCallback<ChangeList> callback) {
+ RestApi call = newQuery(query);
+ if (limit > 0) {
+ call.addParameter("n", limit);
+ }
+ addOptions(call, ListChangesOption.LABELS);
+ if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
+ call.addParameter("P", sortkey);
+ }
+ call.send(callback);
+ }
+
+ public static void next(String query,
+ int limit, String sortkey,
+ AsyncCallback<ChangeList> callback) {
+ RestApi call = newQuery(query);
+ if (limit > 0) {
+ call.addParameter("n", limit);
+ }
+ addOptions(call, ListChangesOption.LABELS);
+ if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
+ call.addParameter("N", sortkey);
+ }
+ call.send(callback);
+ }
+
+ private static void addOptions(
+ RestApi call, ListChangesOption option1, ListChangesOption... options) {
+ EnumSet<ListChangesOption> s = EnumSet.of(option1, options);
+ call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
+ }
+
+ private static RestApi newQuery(String query) {
+ RestApi call = new RestApi(URI);
+ // The server default is ?q=status:open so don't repeat it.
+ if (!"status:open".equals(query) && !"is:open".equals(query)) {
+ call.addParameterRaw("q", KeyUtil.encode(query));
+ }
+ return call;
+ }
+
+ protected ChangeList() {
+ }
+}
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 41dc6c1a92..4bd0828ea9 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
@@ -18,8 +18,6 @@ import com.google.gwt.i18n.client.Messages;
public interface ChangeMessages extends Messages {
String accountDashboardTitle(String fullName);
- String changesStartedBy(String fullName);
- String changesReviewableBy(String fullName);
String changesOpenInProject(String string);
String changesMergedInProject(String string);
String changesAbandonedInProject(String string);
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 40088a12ef..2449613926 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
@@ -1,6 +1,4 @@
accountDashboardTitle = Code Review Dashboard for {0}
-changesStartedBy = Started by {0}
-changesReviewableBy = Review Requests for {0}
changesOpenInProject = Open Changes In {0}
changesMergedInProject = Merged Changes In {0}
changesAbandonedInProject = Abandoned Changes In {0}
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 8d5e105172..4dd6b033a0 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
@@ -28,9 +28,9 @@ import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -42,7 +42,6 @@ import com.google.gwt.user.client.ui.DisclosurePanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
@@ -60,9 +59,7 @@ public class ChangeScreen extends Screen
private final Change.Id changeId;
private final PatchSet.Id openPatchSetId;
private ChangeDetailCache detailCache;
- private StarCache starred;
- private Image starChange;
private ChangeDescriptionBlock descriptionBlock;
private ApprovalTable approvals;
@@ -118,14 +115,6 @@ public class ChangeScreen extends Screen
}
@Override
- public void onSignOut() {
- super.onSignOut();
- if (starChange != null) {
- starChange.setVisible(false);
- }
- }
-
- @Override
protected void onLoad() {
super.onLoad();
detailCache.refresh();
@@ -163,9 +152,8 @@ public class ChangeScreen extends Screen
detailCache = cache.getChangeDetailCache();
detailCache.addValueChangeHandler(this);
- starred = cache.getStarCache();
-
addStyleName(Gerrit.RESOURCES.css().changeScreen());
+ addStyleName(Gerrit.RESOURCES.css().screenNoHeader());
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
@@ -173,16 +161,11 @@ public class ChangeScreen extends Screen
keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
if (Gerrit.isSignedIn()) {
- keysAction.add(starred.new KeyCommand(0, 's', Util.C.changeTableStar()));
keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
.keyPublishComments()));
-
- starChange = starred.createStar();
- starChange.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
- setTitleWest(starChange);
}
- descriptionBlock = new ChangeDescriptionBlock();
+ descriptionBlock = new ChangeDescriptionBlock(keysAction);
add(descriptionBlock);
approvals = new ApprovalTable();
@@ -200,6 +183,23 @@ public class ChangeScreen extends Screen
}
};
dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn());
+ dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() {
+ @Override
+ public String getRowStyle(ChangeInfo c) {
+ if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) {
+ return Gerrit.RESOURCES.css().outdated();
+ }
+ return null;
+ }
+
+ @Override
+ public String getDisplayText(final ChangeInfo c, final String displayText) {
+ if (! c.isLatest()) {
+ return displayText + " [OUTDATED]";
+ }
+ return displayText;
+ }
+ });
neededBy = new ChangeTable.Section(Util.C.changeScreenNeededBy());
dependencies.addSection(dependsOn);
dependencies.addSection(neededBy);
@@ -256,6 +256,7 @@ public class ChangeScreen extends Screen
}
}
setPageTitle(titleBuf.toString());
+ setHeaderVisible(false);
}
@Override
@@ -278,8 +279,10 @@ public class ChangeScreen extends Screen
dependencies.setAccountInfoCache(detail.getAccounts());
approvals.setAccountInfoCache(detail.getAccounts());
- descriptionBlock.display(detail.getChange(), detail
- .getCurrentPatchSetDetail().getInfo(), detail.getAccounts());
+ descriptionBlock.display(detail.getChange(),
+ detail.isStarred(),
+ detail.getCurrentPatchSetDetail().getInfo(),
+ detail.getAccounts());
dependsOn.display(detail.getDependsOn());
neededBy.display(detail.getNeededBy());
approvals.display(detail);
@@ -304,17 +307,28 @@ public class ChangeScreen extends Screen
addComments(detail);
// If any dependency change is still open, or is outdated,
+ // or the change is needed by a change that is new or submitted,
// show our dependency list.
//
boolean depsOpen = false;
int outdated = 0;
- if (!detail.getChange().getStatus().isClosed()
- && detail.getDependsOn() != null) {
- for (final ChangeInfo ci : detail.getDependsOn()) {
- if (! ci.isLatest()) {
- depsOpen = true;
- outdated++;
- } else if (ci.getStatus() != Change.Status.MERGED) {
+ if (!detail.getChange().getStatus().isClosed()) {
+ if (detail.getDependsOn() != null) {
+ for (final ChangeInfo ci : detail.getDependsOn()) {
+ if (!ci.isLatest()) {
+ depsOpen = true;
+ outdated++;
+ } else if (ci.getStatus() != Change.Status.MERGED) {
+ depsOpen = true;
+ }
+ }
+ }
+ }
+ if (detail.getNeededBy() != null) {
+ for (final ChangeInfo ci : detail.getNeededBy()) {
+ if ((ci.getStatus() == Change.Status.NEW) ||
+ (ci.getStatus() == Change.Status.SUBMITTED) ||
+ (ci.getStatus() == Change.Status.DRAFT)) {
depsOpen = true;
}
}
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 5a568f1629..ef4ef52771 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
@@ -20,7 +20,7 @@ import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.NavigationTable;
@@ -145,7 +145,7 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
protected void onStarClick(final int row) {
final ChangeInfo c = getRowItem(row);
if (c != null && Gerrit.isSignedIn()) {
- ChangeCache.get(c.getId()).getStarCache().toggleStar();
+ ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
}
}
@@ -191,29 +191,29 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
}
}
- private void populateChangeRow(final int row, final ChangeInfo c) {
+ private void populateChangeRow(final int row, final ChangeInfo c,
+ final ChangeRowFormatter changeRowFormatter) {
ChangeCache cache = ChangeCache.get(c.getId());
cache.getChangeInfoCache().set(c);
final String idstr = c.getKey().abbreviate();
table.setWidget(row, C_ARROW, null);
if (Gerrit.isSignedIn()) {
- table.setWidget(row, C_STAR, cache.getStarCache().createStar());
+ table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
}
table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
- String s = c.getSubject();
- if (s.length() > 80) {
- s = s.substring(0, 80);
- }
+ String s = Util.cropSubject(c.getSubject());
if (c.getStatus() != null && c.getStatus() != Change.Status.NEW) {
s += " (" + c.getStatus().name() + ")";
}
- if (! c.isLatest()) {
- s += " [OUTDATED]";
- table.getRowFormatter().addStyleName(row, Gerrit.RESOURCES.css().outdated());
- } else {
- table.getRowFormatter().removeStyleName(row, Gerrit.RESOURCES.css().outdated());
+ if (changeRowFormatter != null) {
+ removeChangeStyle(row, changeRowFormatter);
+ final String rowStyle = changeRowFormatter.getRowStyle(c);
+ if (rowStyle != null) {
+ table.getRowFormatter().addStyleName(row, rowStyle);
+ }
+ s = changeRowFormatter.getDisplayText(c, s);
}
table.setWidget(row, C_SUBJECT, new TableChangeLink(s, c));
@@ -223,11 +223,25 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
table.setWidget(row, C_BRANCH, new BranchLink(c.getProject().getKey(), c
.getStatus(), c.getBranch(), c.getTopic()));
table.setText(row, C_LAST_UPDATE, shortFormat(c.getLastUpdatedOn()));
+
setRowItem(row, c);
}
- private AccountDashboardLink link(final Account.Id id) {
- return AccountDashboardLink.link(accountCache, id);
+ private void removeChangeStyle(int row,
+ final ChangeRowFormatter changeRowFormatter) {
+ final ChangeInfo oldChange = getRowItem(row);
+ if (oldChange == null) {
+ return;
+ }
+
+ final String oldRowStyle = changeRowFormatter.getRowStyle(oldChange);
+ if (oldRowStyle != null) {
+ table.getRowFormatter().removeStyleName(row, oldRowStyle);
+ }
+ }
+
+ private AccountLink link(final Account.Id id) {
+ return AccountLink.link(accountCache, id);
}
public void addSection(final Section s) {
@@ -286,13 +300,13 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
int col = BASE_COLUMNS;
boolean haveReview = false;
- boolean displayPersonNameInReviewCategory = false;
+ boolean showUsernameInReviewCategory = false;
if (Gerrit.isSignedIn()) {
AccountGeneralPreferences prefs = Gerrit.getUserAccount().getGeneralPreferences();
- if (prefs.isDisplayPersonNameInReviewCategory()) {
- displayPersonNameInReviewCategory = true;
+ if (prefs.isShowUsernameInReviewCategory()) {
+ showUsernameInReviewCategory = true;
}
}
@@ -314,7 +328,7 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
if (type.isMaxNegative(ca)) {
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
FlowPanel fp = new FlowPanel();
fp.add(new Image(Gerrit.RESOURCES.redNot()));
fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -325,7 +339,7 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
} else if (type.isMaxPositive(ca)) {
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
FlowPanel fp = new FlowPanel();
fp.add(new Image(Gerrit.RESOURCES.greenCheck()));
fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -337,7 +351,7 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
} else {
String vstr = String.valueOf(ca.getValue());
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
vstr = vstr + " " + FormatUtil.name(ai);
}
@@ -419,6 +433,8 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
int dataBegin;
int rows;
+ private ChangeRowFormatter changeRowFormatter;
+
public Section() {
this(null, ApprovalViewType.NONE, null);
}
@@ -441,6 +457,10 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
}
}
+ public void setChangeRowFormatter(final ChangeRowFormatter changeRowFormatter) {
+ this.changeRowFormatter = changeRowFormatter;
+ }
+
public void display(final List<ChangeInfo> changeList) {
final int sz = changeList != null ? changeList.size() : 0;
final boolean hadData = rows > 0;
@@ -470,7 +490,7 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
for (int i = 0; i < sz; i++) {
ChangeInfo c = changeList.get(i);
- parent.populateChangeRow(dataBegin + i, c);
+ parent.populateChangeRow(dataBegin + i, c, changeRowFormatter);
cids.add(c.getId());
}
@@ -489,4 +509,25 @@ public class ChangeTable extends NavigationTable<ChangeInfo> {
}
}
}
+
+ public static interface ChangeRowFormatter {
+ /**
+ * Returns the name of the CSS style that should be applied to the change
+ * row.
+ *
+ * @param c the change for which the styling should be returned
+ * @return the name of the CSS style that should be applied to the change
+ * row
+ */
+ String getRowStyle(ChangeInfo c);
+
+ /**
+ * Returns the text that should be displayed for the change.
+ *
+ * @param c the change for which the display text should be returned
+ * @param displayText the current display text
+ * @return the new display text
+ */
+ String getDisplayText(ChangeInfo c, String displayText);
+ }
}
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
new file mode 100644
index 0000000000..20dd80f3a7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -0,0 +1,416 @@
+// 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.client.changes;
+
+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.BranchLink;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
+import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeTable2 extends NavigationTable<ChangeInfo> {
+ private static final int C_STAR = 1;
+ private static final int C_ID = 2;
+ private static final int C_SUBJECT = 3;
+ private static final int C_OWNER = 4;
+ private static final int C_PROJECT = 5;
+ private static final int C_BRANCH = 6;
+ private static final int C_LAST_UPDATE = 7;
+ private static final int BASE_COLUMNS = 8;
+
+ private final List<Section> sections;
+ private int columns;
+ private List<String> labelNames;
+
+ public ChangeTable2() {
+ columns = BASE_COLUMNS;
+ labelNames = Collections.emptyList();
+
+ keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
+ keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
+ keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
+ keysNavigation.add(
+ new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.changeTableOpen()));
+
+ if (Gerrit.isSignedIn()) {
+ keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+ }
+
+ sections = new ArrayList<Section>();
+ table.setText(0, C_STAR, "");
+ table.setText(0, C_ID, Util.C.changeTableColumnID());
+ table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+ table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
+ table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
+ table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
+ table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
+
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
+ fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
+ for (int i = C_ID; i < columns; i++) {
+ fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
+ }
+
+ table.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ final Cell cell = table.getCellForEvent(event);
+ if (cell == null) {
+ return;
+ }
+ if (cell.getCellIndex() == C_STAR) {
+ // Don't do anything (handled by star itself).
+ } else if (cell.getCellIndex() == C_OWNER) {
+ // Don't do anything.
+ } else if (getRowItem(cell.getRowIndex()) != null) {
+ movePointerTo(cell.getRowIndex());
+ }
+ }
+ });
+ }
+
+ @Override
+ protected Object getRowItemKey(final ChangeInfo item) {
+ return item.legacy_id();
+ }
+
+ @Override
+ protected void onOpenRow(final int row) {
+ final ChangeInfo c = getRowItem(row);
+ final Change.Id id = c.legacy_id();
+ Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+ }
+
+ private void insertNoneRow(final int row) {
+ insertRow(row);
+ table.setText(row, 0, Util.C.changeTableNone());
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.setColSpan(row, 0, columns);
+ fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
+ }
+
+ private void insertChangeRow(final int row) {
+ insertRow(row);
+ applyDataRowStyle(row);
+ }
+
+ @Override
+ protected void applyDataRowStyle(final int row) {
+ super.applyDataRowStyle(row);
+ final CellFormatter fmt = table.getCellFormatter();
+ fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
+ for (int i = C_ID; i < columns; i++) {
+ fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
+ }
+ fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
+ fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
+ fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
+ fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+ fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
+ for (int i = BASE_COLUMNS; i < columns; i++) {
+ fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
+ }
+ }
+
+ public void updateColumnsForLabels(ChangeList... lists) {
+ labelNames = new ArrayList<String>();
+ for (ChangeList list : lists) {
+ for (int i = 0; i < list.size(); i++) {
+ for (String name : list.get(i).labels()) {
+ if (!labelNames.contains(name)) {
+ labelNames.add(name);
+ }
+ }
+ }
+ }
+ Collections.sort(labelNames);
+
+ if (BASE_COLUMNS + labelNames.size() < columns) {
+ int n = columns - (BASE_COLUMNS + labelNames.size());
+ for (int row = 0; row < table.getRowCount(); row++) {
+ table.removeCells(row, columns, n);
+ }
+ }
+ columns = BASE_COLUMNS + labelNames.size();
+
+ FlexCellFormatter fmt = table.getFlexCellFormatter();
+ for (int i = 0; i < labelNames.size(); i++) {
+ String name = labelNames.get(i);
+ int col = BASE_COLUMNS + i;
+
+ StringBuilder abbrev = new StringBuilder();
+ for (String t : name.split("-")) {
+ abbrev.append(t.substring(0, 1).toUpperCase());
+ }
+ table.setText(0, col, abbrev.toString());
+ table.getCellFormatter().getElement(0, col).setTitle(name);
+ fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
+ }
+
+ for (Section s : sections) {
+ if (s.titleRow >= 0) {
+ fmt.setColSpan(s.titleRow, 0, columns);
+ }
+ }
+ }
+
+ private void populateChangeRow(final int row, final ChangeInfo c,
+ boolean highlightUnreviewed) {
+ if (Gerrit.isSignedIn()) {
+ table.setWidget(row, C_STAR, StarredChanges.createIcon(
+ c.legacy_id(),
+ c.starred()));
+ }
+ table.setWidget(row, C_ID, new TableChangeLink(c.id_abbreviated(), c));
+
+ String subject = Util.cropSubject(c.subject());
+ Change.Status status = c.status();
+ if (status != Change.Status.NEW) {
+ subject += " (" + Util.toLongString(status) + ")";
+ }
+ table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
+ String owner = "";
+ if (c.owner() != null && c.owner().name() != null) {
+ owner = c.owner().name();
+ }
+
+ table.setWidget(row, C_OWNER, new InlineHyperlink(owner,
+ PageLinks.toAccountQuery(owner, c.status())));
+
+ table.setWidget(
+ 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()));
+
+ boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
+ .getGeneralPreferences().isShowUsernameInReviewCategory();
+
+ CellFormatter fmt = table.getCellFormatter();
+ for (int idx = 0; idx < labelNames.size(); idx++) {
+ String name = labelNames.get(idx);
+ int col = BASE_COLUMNS + idx;
+
+ LabelInfo label = c.label(name);
+ if (label == null) {
+ table.clearCell(row, col);
+ continue;
+ }
+
+ String user;
+ if (label.rejected() != null) {
+ user = label.rejected().name();
+ if (displayName && user != null) {
+ FlowPanel panel = new FlowPanel();
+ panel.add(new Image(Gerrit.RESOURCES.redNot()));
+ panel.add(new InlineLabel(user));
+ table.setWidget(row, col, panel);
+ } else {
+ table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
+ }
+ } else if (label.approved() != null) {
+ user = label.approved().name();
+ if (displayName && user != null) {
+ FlowPanel panel = new FlowPanel();
+ panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
+ panel.add(new InlineLabel(user));
+ table.setWidget(row, col, panel);
+ } else {
+ table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
+ }
+ } else if (label.disliked() != null) {
+ user = label.disliked().name();
+ String vstr = String.valueOf(label._value());
+ if (displayName && user != null) {
+ vstr = vstr + " " + user;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
+ table.setText(row, col, vstr);
+ } else if (label.recommended() != null) {
+ user = label.recommended().name();
+ String vstr = "+" + label._value();
+ if (displayName && user != null) {
+ vstr = vstr + " " + user;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
+ table.setText(row, col, vstr);
+ } else {
+ table.clearCell(row, col);
+ continue;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
+
+ if (!displayName && user != null) {
+ // Some web browsers ignore the embedded newline; some like it;
+ // so we include a space before the newline to accommodate both.
+ fmt.getElement(row, col).setTitle(name + " \nby " + user);
+ }
+ }
+
+ boolean needHighlight = false;
+ if (highlightUnreviewed && !c.reviewed()) {
+ needHighlight = true;
+ }
+ final Element tr = DOM.getParent(fmt.getElement(row, 0));
+ UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+ needHighlight);
+
+ setRowItem(row, c);
+ }
+
+ public void addSection(final Section s) {
+ assert s.parent == null;
+
+ if (s.titleText != null) {
+ s.titleRow = table.getRowCount();
+ table.setText(s.titleRow, 0, s.titleText);
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.setColSpan(s.titleRow, 0, columns);
+ fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
+ } else {
+ s.titleRow = -1;
+ }
+
+ s.parent = this;
+ s.dataBegin = table.getRowCount();
+ insertNoneRow(s.dataBegin);
+ sections.add(s);
+ }
+
+ private int insertRow(final int beforeRow) {
+ for (final Section s : sections) {
+ if (beforeRow <= s.titleRow) {
+ s.titleRow++;
+ }
+ if (beforeRow < s.dataBegin) {
+ s.dataBegin++;
+ }
+ }
+ return table.insertRow(beforeRow);
+ }
+
+ private void removeRow(final int row) {
+ for (final Section s : sections) {
+ if (row < s.titleRow) {
+ s.titleRow--;
+ }
+ if (row < s.dataBegin) {
+ s.dataBegin--;
+ }
+ }
+ table.removeRow(row);
+ }
+
+ public class StarKeyCommand extends NeedsSignInKeyCommand {
+ public StarKeyCommand(int mask, char key, String help) {
+ super(mask, key, help);
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ int row = getCurrentRow();
+ ChangeInfo c = getRowItem(row);
+ if (c != null && Gerrit.isSignedIn()) {
+ ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
+ }
+ }
+ }
+
+ private final class TableChangeLink extends ChangeLink {
+ private TableChangeLink(final String text, final ChangeInfo c) {
+ super(text, c.legacy_id());
+ }
+
+ @Override
+ public void go() {
+ movePointerTo(cid);
+ super.go();
+ }
+ }
+
+ public static class Section {
+ ChangeTable2 parent;
+ String titleText;
+ int titleRow = -1;
+ int dataBegin;
+ int rows;
+ private boolean highlightUnreviewed;
+
+ public void setHighlightUnreviewed(boolean value) {
+ this.highlightUnreviewed = value;
+ }
+
+ public void setTitleText(final String text) {
+ titleText = text;
+ if (titleRow >= 0) {
+ parent.table.setText(titleRow, 0, titleText);
+ }
+ }
+
+ public void display(ChangeList changeList) {
+ final int sz = changeList != null ? changeList.size() : 0;
+ final boolean hadData = rows > 0;
+
+ if (hadData) {
+ while (sz < rows) {
+ parent.removeRow(dataBegin);
+ rows--;
+ }
+ } else {
+ parent.removeRow(dataBegin);
+ }
+
+ if (sz == 0) {
+ parent.insertNoneRow(dataBegin);
+ return;
+ }
+
+ while (rows < sz) {
+ parent.insertChangeRow(dataBegin + rows);
+ rows++;
+ }
+ for (int i = 0; i < sz; i++) {
+ parent.populateChangeRow(dataBegin + i, changeList.get(i),
+ highlightUnreviewed);
+ }
+ }
+ }
+}
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 1a6ea3e11f..eff3cd59df 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
@@ -15,28 +15,98 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.PreElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
public class CommitMessageBlock extends Composite {
- private final HTML description;
+ interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
+ }
+
+ private static Binder uiBinder = GWT.create(Binder.class);
+
+ private KeyCommandSet keysAction;
+
+ @UiField
+ SimplePanel starPanel;
+ @UiField
+ FlowPanel permalinkPanel;
+ @UiField
+ PreElement commitSummaryPre;
+ @UiField
+ PreElement commitBodyPre;
public CommitMessageBlock() {
- description = new HTML();
- description.setStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
- initWidget(description);
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ public CommitMessageBlock(KeyCommandSet keysAction) {
+ this.keysAction = keysAction;
+ initWidget(uiBinder.createAndBindUi(this));
}
public void display(final String commitMessage) {
- SafeHtml msg = new SafeHtmlBuilder().append(commitMessage);
- msg = msg.linkify();
- msg = CommentLinkProcessor.apply(msg);
- msg = new SafeHtmlBuilder().openElement("p").append(msg).closeElement("p");
- msg = msg.replaceAll("\n\n", "</p><p>");
- msg = msg.replaceAll("\n", "<br />");
- SafeHtml.set(description, msg);
+ display(null, null, commitMessage);
+ }
+
+ public void display(Change.Id changeId, Boolean starred, String commitMessage) {
+ starPanel.clear();
+
+ if (changeId != null && starred != null && Gerrit.isSignedIn()) {
+ StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
+ star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
+ starPanel.add(star);
+
+ if (keysAction != null) {
+ keysAction.add(StarredChanges.newKeyCommand(star));
+ }
+ }
+
+ permalinkPanel.clear();
+ if (changeId != null) {
+ permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
+ permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId), false));
+ }
+
+ String[] splitCommitMessage = commitMessage.split("\n", 2);
+
+ String commitSummary = splitCommitMessage[0];
+ String commitBody = "";
+ if (splitCommitMessage.length > 1) {
+ commitBody = splitCommitMessage[1];
+ }
+
+ // Linkify commit summary
+ SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
+ commitSummaryLinkified = commitSummaryLinkified.linkify();
+ commitSummaryLinkified = CommentLinkProcessor.apply(commitSummaryLinkified);
+ commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
+
+ // Hide commit body if there is no body
+ if (commitBody.trim().isEmpty()) {
+ commitBodyPre.getStyle().setDisplay(Display.NONE);
+ } else {
+ // Linkify commit body
+ SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
+ commitBodyLinkified = commitBodyLinkified.linkify();
+ 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/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml
new file mode 100644
index 0000000000..ca81537927
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+ <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+ <ui:style>
+ @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+ @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+ .commitMessageTable {
+ border-collapse: separate;
+ border-spacing: 0;
+ margin-bottom: 10px;
+ }
+
+ .header {
+ background-color: trimColor;
+ white-space: nowrap;
+ color: textColor;
+ font-size: 10pt;
+ font-style: italic;
+ padding: 2px 6px 1px;
+ }
+
+ .contents {
+ border-bottom: 1px solid trimColor;
+ border-left: 1px solid trimColor;
+ border-right: 1px solid trimColor;
+ padding: 5px;
+ }
+
+ .contents span {
+ font-weight: bold;
+ }
+
+ .contents pre {
+ margin: 0;
+ }
+
+ .commitSummary {
+ font-weight: bold;
+ }
+
+ .commitBody p {
+ padding-top: 0px;
+ }
+
+ .starPanel {
+ float: left;
+ }
+
+ .boxTitle {
+ float: left;
+ margin-right: 10px;
+ }
+
+ .permalinkPanel {
+ float: right;
+ }
+
+ .permalinkPanel a {
+ float: left;
+ }
+
+ .permalinkPanel div {
+ display: inline;
+ }
+ </ui:style>
+
+ <g:HTMLPanel>
+ <table class='{style.commitMessageTable}'>
+ <tr><td class='{style.header}'>
+ <g:SimplePanel styleName='{style.starPanel}' ui:field='starPanel'></g:SimplePanel>
+ <div class='{style.boxTitle}'>Commit Message</div>
+ <g:FlowPanel styleName='{style.permalinkPanel}' ui:field='permalinkPanel'></g:FlowPanel>
+ </td></tr>
+ <tr><td class='{style.contents}'>
+ <pre class='{style.commitSummary} {res.css.changeScreenDescription}' ui:field='commitSummaryPre'/>
+ <pre class='{style.commitBody} {res.css.changeScreenDescription}' ui:field='commitBodyPre'/>
+ </td></tr>
+ </table>
+ </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
new file mode 100644
index 0000000000..c9f1dc6199
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -0,0 +1,112 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gwt.http.client.URL;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomDashboardScreen extends Screen implements ChangeListScreen {
+ private String title;
+ private List<String> titles;
+ private List<String> queries;
+ private ChangeTable2 table;
+ private List<ChangeTable2.Section> sections;
+
+ public CustomDashboardScreen(String params) {
+ titles = new ArrayList<String>();
+ queries = new ArrayList<String>();
+ for (String kvPair : params.split("[,;&]")) {
+ String[] kv = kvPair.split("=", 2);
+ if (kv.length != 2 || kv[0].isEmpty()) {
+ continue;
+ }
+
+ if ("title".equals(kv[0])) {
+ title = URL.decodeQueryString(kv[1]);
+ } else {
+ titles.add(URL.decodeQueryString(kv[0]));
+ queries.add(URL.decodeQueryString(kv[1]));
+ }
+ }
+ }
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+
+ if (title != null) {
+ setWindowTitle(title);
+ setPageTitle(title);
+ }
+
+ table = new ChangeTable2();
+ table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
+
+ sections = new ArrayList<ChangeTable2.Section>();
+ for (String title : titles) {
+ ChangeTable2.Section s = new ChangeTable2.Section();
+ s.setTitleText(title);
+ table.addSection(s);
+ sections.add(s);
+ }
+ add(table);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ if (queries.isEmpty()) {
+ display();
+ } else if (queries.size() == 1) {
+ ChangeList.next(queries.get(0),
+ 0, PagedSingleListScreen.MAX_SORTKEY,
+ new ScreenLoadCallback<ChangeList>(this) {
+ @Override
+ protected void preDisplay(ChangeList result) {
+ table.updateColumnsForLabels(result);
+ sections.get(0).display(result);
+ table.finishDisplay();
+ }
+ });
+ } else {
+ ChangeList.query(
+ new ScreenLoadCallback<NativeList<ChangeList>>(this) {
+ @Override
+ protected void preDisplay(NativeList<ChangeList> result) {
+ table.updateColumnsForLabels(
+ result.asList().toArray(new ChangeList[result.size()]));
+ for (int i = 0; i < result.size(); i++) {
+ sections.get(i).display(result.get(i));
+ }
+ table.finishDisplay();
+ }
+ },
+ queries.toArray(new String[queries.size()]));
+ }
+ }
+
+ @Override
+ public void registerKeys() {
+ super.registerKeys();
+ table.setRegisterKeys(true);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 72300b2d99..23ce1789be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -15,12 +15,9 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.user.client.History;
@@ -28,19 +25,16 @@ import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwtexpui.globalkey.client.KeyCommand;
-import java.util.List;
-
-
public abstract class PagedSingleListScreen extends Screen {
protected static final String MIN_SORTKEY = "";
protected static final String MAX_SORTKEY = "z";
protected final int pageSize;
- private ChangeTable table;
- private ChangeTable.Section section;
+ private ChangeTable2 table;
+ private ChangeTable2.Section section;
protected Hyperlink prev;
protected Hyperlink next;
- protected List<ChangeInfo> changes;
+ protected ChangeList changes;
protected final String anchorPrefix;
protected boolean useLoadPrev;
@@ -71,7 +65,7 @@ public abstract class PagedSingleListScreen extends Screen {
next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
next.setVisible(false);
- table = new ChangeTable(true) {
+ table = new ChangeTable2() {
{
keysNavigation.add(new DoLinkCommand(0, 'p', Util.C
.changeTablePagePrev(), prev));
@@ -79,8 +73,7 @@ public abstract class PagedSingleListScreen extends Screen {
.changeTablePageNext(), next));
}
};
- section = new ChangeTable.Section(null, ApprovalViewType.STRONGEST, null);
-
+ section = new ChangeTable2.Section();
table.addSection(section);
table.setSavePointerId(anchorPrefix);
add(table);
@@ -112,36 +105,34 @@ public abstract class PagedSingleListScreen extends Screen {
protected abstract void loadNext();
- protected AsyncCallback<SingleListChangeInfo> loadCallback() {
- return new ScreenLoadCallback<SingleListChangeInfo>(this) {
+ protected AsyncCallback<ChangeList> loadCallback() {
+ return new ScreenLoadCallback<ChangeList>(this) {
@Override
- protected void preDisplay(final SingleListChangeInfo result) {
+ protected void preDisplay(ChangeList result) {
display(result);
}
};
}
- protected void display(final SingleListChangeInfo result) {
- changes = result.getChanges();
-
+ protected void display(final ChangeList result) {
+ changes = result;
if (!changes.isEmpty()) {
final ChangeInfo f = changes.get(0);
final ChangeInfo l = changes.get(changes.size() - 1);
- prev.setTargetHistoryToken(anchorPrefix + ",p," + f.getSortKey());
- next.setTargetHistoryToken(anchorPrefix + ",n," + l.getSortKey());
+ prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
+ next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
if (useLoadPrev) {
- prev.setVisible(!result.isAtEnd());
+ prev.setVisible(f._more_changes());
next.setVisible(!MIN_SORTKEY.equals(pos));
} else {
prev.setVisible(!MAX_SORTKEY.equals(pos));
- next.setVisible(!result.isAtEnd());
+ next.setVisible(l._more_changes());
}
}
-
- table.setAccountInfoCache(result.getAccounts());
- section.display(result.getChanges());
+ table.updateColumnsForLabels(result);
+ section.display(result);
table.finishDisplay();
}
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 81b4f140d5..7e314cc340 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
@@ -20,16 +20,16 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.CommentedActionDialog;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
+import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
@@ -128,6 +128,12 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
* followed by the action buttons.
*/
public void ensureLoaded(final PatchSetDetail detail) {
+ loadInfoTable(detail);
+ loadActionPanel(detail);
+ loadPatchTable(detail);
+ }
+
+ public void loadInfoTable(final PatchSetDetail detail) {
infoTable = new Grid(R_CNT, 2);
infoTable.setStyleName(Gerrit.RESOURCES.css().infoBlock());
infoTable.addStyleName(Gerrit.RESOURCES.css().patchSetInfoBlock());
@@ -153,15 +159,13 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
displayDownload();
body.add(infoTable);
+ }
+ public void loadActionPanel(final PatchSetDetail detail) {
if (!patchSet.getId().equals(diffBaseId)) {
- patchTable = new PatchTable();
- patchTable.setSavePointerId("PatchTable " + patchSet.getId());
- patchTable.display(diffBaseId, detail);
-
actionsPanel = new FlowPanel();
actionsPanel.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
- body.add(actionsPanel);
+ actionsPanel.setVisible(true);
if (Gerrit.isSignedIn()) {
if (changeDetail.canEdit()) {
populateReviewAction();
@@ -173,18 +177,28 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
if (changeDetail.canPublish()) {
populatePublishAction();
}
- if (changeDetail.canDeleteDraft() &&
- changeDetail.getPatchSets().size() > 1) {
+ if (changeDetail.canDeleteDraft()
+ && changeDetail.getPatchSets().size() > 1) {
populateDeleteDraftPatchSetAction();
}
}
}
populateDiffAllActions(detail);
- body.add(patchTable);
+ body.add(actionsPanel);
+ }
+ }
- for(ClickHandler clickHandler : registeredClickHandler) {
+ public void loadPatchTable(final PatchSetDetail detail) {
+ if (!patchSet.getId().equals(diffBaseId)) {
+ patchTable = new PatchTable();
+ patchTable.setSavePointerId("PatchTable " + patchSet.getId());
+ patchTable.display(diffBaseId, detail);
+ for (ClickHandler clickHandler : registeredClickHandler) {
patchTable.addClickHandler(clickHandler);
}
+ patchTable.setRegisterKeys(true);
+ setActive(true);
+ body.add(patchTable);
}
}
@@ -195,6 +209,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
final DownloadCommandPanel commands = new DownloadCommandPanel();
final DownloadUrlPanel urls = new DownloadUrlPanel(commands);
final Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
+ final Set<DownloadCommand> allowedCommands = Gerrit.getConfig().getDownloadCommands();
copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
@@ -232,9 +247,8 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
.anonymousDownload("HTTP"), r.toString()));
}
- if (Gerrit.getConfig().getSshdAddress() != null && Gerrit.isSignedIn()
- && Gerrit.getUserAccount().getUserName() != null
- && Gerrit.getUserAccount().getUserName().length() > 0
+ if (Gerrit.getConfig().getSshdAddress() != null
+ && hasUserName()
&& (allowedSchemes.contains(DownloadScheme.SSH) ||
allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
String sshAddr = Gerrit.getConfig().getSshdAddress();
@@ -256,13 +270,12 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
urls.add(new DownloadUrlLink(DownloadScheme.SSH, "SSH", r.toString()));
}
- if (Gerrit.isSignedIn() && Gerrit.getUserAccount().getUserName() != null
- && Gerrit.getUserAccount().getUserName().length() > 0
- && (allowedSchemes.contains(DownloadScheme.HTTP) ||
- allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+ if ((hasUserName() || siteReliesOnHttp())
+ && (allowedSchemes.contains(DownloadScheme.HTTP)
+ || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
final StringBuilder r = new StringBuilder();
if (Gerrit.getConfig().getGitHttpUrl() != null
- && changeDetail.isAllowsAnonymous()) {
+ && (changeDetail.isAllowsAnonymous() || siteReliesOnHttp())) {
r.append(Gerrit.getConfig().getGitHttpUrl());
} else {
String base = hostPageUrl;
@@ -311,39 +324,52 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
}
if (!urls.isEmpty()) {
- commands.add(new DownloadCommandLink(DownloadCommand.CHECKOUT, "checkout") {
- @Override
- void setCurrentUrl(DownloadUrlLink link) {
- urls.setVisible(true);
- copyLabel.setText("git fetch " + link.urlData
- + " && git checkout FETCH_HEAD");
- }
- });
- commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
- @Override
- void setCurrentUrl(DownloadUrlLink link) {
- urls.setVisible(true);
- copyLabel.setText("git pull " + link.urlData);
- }
- });
- commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
- "cherry-pick") {
- @Override
- void setCurrentUrl(DownloadUrlLink link) {
- urls.setVisible(true);
- copyLabel.setText("git fetch " + link.urlData
- + " && git cherry-pick FETCH_HEAD");
- }
- });
- commands.add(new DownloadCommandLink(DownloadCommand.FORMAT_PATCH,
- "patch") {
- @Override
- void setCurrentUrl(DownloadUrlLink link) {
- urls.setVisible(true);
- copyLabel.setText("git fetch " + link.urlData
- + " && git format-patch -1 --stdout FETCH_HEAD");
- }
- });
+ if (allowedCommands.contains(DownloadCommand.CHECKOUT)
+ || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+ commands.add(new DownloadCommandLink(DownloadCommand.CHECKOUT,
+ "checkout") {
+ @Override
+ void setCurrentUrl(DownloadUrlLink link) {
+ urls.setVisible(true);
+ copyLabel.setText("git fetch " + link.urlData
+ + " && git checkout FETCH_HEAD");
+ }
+ });
+ }
+ if (allowedCommands.contains(DownloadCommand.PULL)
+ || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+ commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
+ @Override
+ void setCurrentUrl(DownloadUrlLink link) {
+ urls.setVisible(true);
+ copyLabel.setText("git pull " + link.urlData);
+ }
+ });
+ }
+ if (allowedCommands.contains(DownloadCommand.CHERRY_PICK)
+ || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+ commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
+ "cherry-pick") {
+ @Override
+ void setCurrentUrl(DownloadUrlLink link) {
+ urls.setVisible(true);
+ copyLabel.setText("git fetch " + link.urlData
+ + " && git cherry-pick FETCH_HEAD");
+ }
+ });
+ }
+ if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH)
+ || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+ commands.add(new DownloadCommandLink(DownloadCommand.FORMAT_PATCH,
+ "patch") {
+ @Override
+ void setCurrentUrl(DownloadUrlLink link) {
+ urls.setVisible(true);
+ copyLabel.setText("git fetch " + link.urlData
+ + " && git format-patch -1 --stdout FETCH_HEAD");
+ }
+ });
+ }
}
final FlowPanel fp = new FlowPanel();
@@ -372,6 +398,18 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
infoTable.setWidget(R_DOWNLOAD, 1, fp);
}
+ private static boolean siteReliesOnHttp() {
+ return Gerrit.getConfig().getGitHttpUrl() != null
+ && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+ && !Gerrit.getConfig().siteHasUsernames();
+ }
+
+ private static boolean hasUserName() {
+ return Gerrit.isSignedIn()
+ && Gerrit.getUserAccount().getUserName() != null
+ && Gerrit.getUserAccount().getUserName().length() > 0;
+ }
+
private void displayUserIdentity(final int row, final UserIdentity who) {
if (who == null) {
infoTable.clearCell(row, 1);
@@ -381,9 +419,9 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
final FlowPanel fp = new FlowPanel();
fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
if (who.getName() != null) {
- final Account.Id aId = who.getAccount();
- if (aId != null) {
- fp.add(new AccountDashboardLink(who.getName(), aId));
+ if (who.getAccount() != null) {
+ fp.add(new InlineHyperlink(who.getName(),
+ PageLinks.toAccountQuery(who.getName())));
} else {
final InlineLabel lbl = new InlineLabel(who.getName());
lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
@@ -414,7 +452,8 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
parentsTable.setWidget(row, 0, new InlineLabel(parent.id.get()));
ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().noborder());
ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().monospace());
- parentsTable.setWidget(row, 1, new InlineLabel(parent.shortMessage));
+ parentsTable.setWidget(row, 1,
+ new InlineLabel(Util.cropSubject(parent.shortMessage)));
ptfmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder());
row++;
}
@@ -437,16 +476,10 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
public void onClick(final ClickEvent event) {
b.setEnabled(false);
Util.MANAGE_SVC.submit(patchSet.getId(),
- new GerritCallback<ChangeDetail>() {
+ new ChangeDetailCache.GerritWidgetCallback(b) {
public void onSuccess(ChangeDetail result) {
onSubmitResult(result);
}
-
- @Override
- public void onFailure(Throwable caught) {
- b.setEnabled(true);
- super.onFailure(caught);
- }
});
}
});
@@ -611,17 +644,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
public void onClick(final ClickEvent event) {
b.setEnabled(false);
Util.MANAGE_SVC.publish(patchSet.getId(),
- new GerritCallback<ChangeDetail>() {
- public void onSuccess(ChangeDetail result) {
- detailCache.set(result);
- }
-
- @Override
- public void onFailure(Throwable caught) {
- b.setEnabled(true);
- super.onFailure(caught);
- }
- });
+ new ChangeDetailCache.GerritWidgetCallback(b));
}
});
actionsPanel.add(b);
@@ -634,7 +657,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
public void onClick(final ClickEvent event) {
b.setEnabled(false);
PatchUtil.DETAIL_SVC.deleteDraftPatchSet(patchSet.getId(),
- new GerritCallback<ChangeDetail>() {
+ new ChangeDetailCache.GerritWidgetCallback(b) {
public void onSuccess(final ChangeDetail result) {
if (result != null) {
detailCache.set(result);
@@ -642,12 +665,6 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
Gerrit.display(PageLinks.MINE);
}
}
-
- @Override
- public void onFailure(Throwable caught) {
- b.setEnabled(true);
- super.onFailure(caught);
- }
});
}
});
@@ -655,34 +672,45 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
}
public void refresh() {
- AccountDiffPreference diffPrefs;
- if (patchTable == null) {
- diffPrefs = new ListenableAccountDiffPreference().get();
+ if (patchSet.getId().equals(diffBaseId)) {
+ if (patchTable != null) {
+ patchTable.setVisible(false);
+ }
+ if (actionsPanel != null) {
+ actionsPanel.setVisible(false);
+ }
} else {
- diffPrefs = patchTable.getPreferences().get();
- }
+ if (patchTable != null) {
+ if (patchTable.getBase() == null && diffBaseId == null
+ || patchTable.getBase() != null
+ && patchTable.getBase().equals(diffBaseId)) {
+ actionsPanel.setVisible(true);
+ patchTable.setVisible(true);
+ return;
+ }
+ }
- Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
- new GerritCallback<PatchSetDetail>() {
- @Override
- public void onSuccess(PatchSetDetail result) {
- if (patchSet.getId().equals(diffBaseId)) {
- patchTable.setVisible(false);
- actionsPanel.setVisible(false);
- } else {
- if (patchTable != null) {
- patchTable.removeFromParent();
- }
- patchTable = new PatchTable();
- patchTable.display(diffBaseId, result);
- body.add(patchTable);
+ AccountDiffPreference diffPrefs;
+ if (patchTable == null) {
+ diffPrefs = new ListenableAccountDiffPreference().get();
+ } else {
+ diffPrefs = patchTable.getPreferences().get();
+ patchTable.setVisible(false);
+ }
- for (ClickHandler clickHandler : registeredClickHandler) {
- patchTable.addClickHandler(clickHandler);
+ Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
+ new GerritCallback<PatchSetDetail>() {
+ @Override
+ public void onSuccess(PatchSetDetail result) {
+ if (actionsPanel != null) {
+ actionsPanel.setVisible(true);
+ } else {
+ loadActionPanel(result);
}
+ loadPatchTable(result);
}
- }
- });
+ });
+ }
}
@Override
@@ -698,8 +726,8 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
new GerritCallback<PatchSetDetail>() {
public void onSuccess(final PatchSetDetail result) {
- ensureLoaded(result);
- patchTable.setRegisterKeys(true);
+ loadInfoTable(result);
+ loadActionPanel(result);
}
});
}
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 7e659a1b46..b9ed4e793c 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
@@ -79,7 +79,7 @@ public class PatchSetsBlock extends Composite {
if (Gerrit.isSignedIn()) {
final AccountGeneralPreferences p =
Gerrit.getUserAccount().getGeneralPreferences();
- if (p.isDisplayPatchSetsInReverseOrder()) {
+ if (p.isReversePatchSetOrder()) {
Collections.reverse(patchSets);
}
}
@@ -175,7 +175,6 @@ public class PatchSetsBlock extends Composite {
deactivate();
PatchSetComplexDisclosurePanel patchSetPanel =
patchSetPanels.get(patchSetId);
- patchSetPanel.setOpen(true);
patchSetPanel.setActive(true);
activePatchSetId = patchSetId;
}
@@ -226,6 +225,9 @@ public class PatchSetsBlock extends Composite {
public void onOpen(OpenEvent<DisclosurePanel> event) {
// when a patch set panel is opened by the user
// it should automatically become active
+ PatchSetComplexDisclosurePanel patchSetPanel =
+ patchSetPanels.get(patchSetId);
+ patchSetPanel.refresh();
activate(patchSetId);
}
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 7177525e47..b8acd56155 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
@@ -50,6 +50,28 @@ import java.util.ArrayList;
import java.util.List;
public class PatchTable extends Composite {
+ public interface PatchValidator {
+ /**
+ * Returns true if patch is valid
+ *
+ * @param patch
+ * @return
+ */
+ boolean isValid(Patch patch);
+ }
+
+ public final PatchValidator PREFERENCE_VALIDATOR =
+ new PatchValidator() {
+ @Override
+ public boolean isValid(Patch patch) {
+ return !((listenablePrefs.get().isSkipDeleted()
+ && patch.getChangeType().equals(ChangeType.DELETED))
+ || (listenablePrefs.get().isSkipUncommented()
+ && patch.getCommentCount() == 0));
+ }
+
+ };
+
private final FlowPanel myBody;
private PatchSetDetail detail;
private Command onLoadCommand;
@@ -97,6 +119,10 @@ public class PatchTable extends Composite {
}
}
+ public PatchSet.Id getBase() {
+ return base;
+ }
+
public void setSavePointerId(final String id) {
savePointerId = id;
}
@@ -176,29 +202,32 @@ public class PatchTable extends Composite {
/**
* @return a link to the previous file in this patch set, or null.
*/
- public InlineHyperlink getPreviousPatchLink(int index, PatchScreen.Type patchType) {
- for(index--; index > -1; index--) {
- InlineHyperlink link = createLink(index, patchType, SafeHtml.asis(Util.C
- .prevPatchLinkIcon()), null);
- if (link != null) {
- return link;
- }
+ public InlineHyperlink getPreviousPatchLink(int index,
+ PatchScreen.Type patchType) {
+ int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR);
+ if (previousPatchIndex < 0) {
+ return null;
}
- return null;
+ InlineHyperlink link =
+ createLink(previousPatchIndex, patchType,
+ SafeHtml.asis(Util.C.prevPatchLinkIcon()), null);
+
+ return link;
}
/**
* @return a link to the next file in this patch set, or null.
*/
public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) {
- for(index++; index < patchList.size(); index++) {
- InlineHyperlink link = createLink(index, patchType, null, SafeHtml.asis(Util.C
- .nextPatchLinkIcon()));
- if (link != null) {
- return link;
- }
+ int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR);
+ if (nextPatchIndex < 0) {
+ return null;
}
- return null;
+ InlineHyperlink link =
+ createLink(nextPatchIndex, patchType, null,
+ SafeHtml.asis(Util.C.nextPatchLinkIcon()));
+
+ return link;
}
/**
@@ -208,16 +237,9 @@ public class PatchTable extends Composite {
* @param before A string to display at the beginning of the href text
* @param after A string to display at the end of the href text
*/
- private PatchLink createLink(int index, PatchScreen.Type patchType,
+ public PatchLink createLink(int index, PatchScreen.Type patchType,
SafeHtml before, SafeHtml after) {
Patch patch = patchList.get(index);
- if (( listenablePrefs.get().isSkipDeleted() &&
- patch.getChangeType().equals(ChangeType.DELETED) )
- ||
- ( listenablePrefs.get().isSkipUncommented() &&
- patch.getCommentCount() == 0 ) ) {
- return null;
- }
Key thisKey = patch.getKey();
PatchLink link;
@@ -814,4 +836,75 @@ public class PatchTable extends Composite {
return System.currentTimeMillis() - start > 200;
}
}
+
+
+ /**
+ * Gets the next patch
+ *
+ * @param currentIndex
+ * @param validators
+ * @param loopAround loops back around to the front and traverses if this is
+ * true
+ * @return
+ */
+ public int getNextPatch(int currentIndex, boolean loopAround,
+ PatchValidator... validators) {
+ return getNextPatchHelper(currentIndex, loopAround, detail.getPatches()
+ .size(), validators);
+ }
+
+ /**
+ * Helper function for getNextPatch
+ *
+ * @param currentIndex
+ * @param validators
+ * @param loopAround
+ * @param maxIndex will only traverse up to this index
+ * @return
+ */
+ private int getNextPatchHelper(int currentIndex, boolean loopAround,
+ int maxIndex, PatchValidator... validators) {
+ for (int i = currentIndex + 1; i < maxIndex; i++) {
+ Patch patch = detail.getPatches().get(i);
+ if (patch != null && patchIsValid(patch, validators)) {
+ return i;
+ }
+ }
+
+ if (loopAround) {
+ return getNextPatchHelper(-1, false, currentIndex, validators);
+ }
+
+ return -1;
+ }
+
+ /**
+ * @return the index to the previous patch
+ */
+ public int getPreviousPatch(int currentIndex, PatchValidator... validators) {
+ for (int i = currentIndex - 1; i >= 0; i--) {
+ Patch patch = detail.getPatches().get(i);
+ if (patch != null && patchIsValid(patch, validators)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Helper function that returns whether a patch is valid or not
+ *
+ * @param patch
+ * @param validators
+ * @return whether the patch is valid based on the validators
+ */
+ private boolean patchIsValid(Patch patch, PatchValidator... validators) {
+ for (PatchValidator v : validators) {
+ if (!v.isValid(patch)) {
+ return false;
+ }
+ }
+ return true;
+ }
}
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 0c0849123f..4705aad452 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
@@ -64,6 +64,7 @@ public class PublishCommentScreen extends AccountScreen implements
private final PatchSet.Id patchSetId;
private Collection<ValueRadioButton> approvalButtons;
private ChangeDescriptionBlock descBlock;
+ private ApprovalTable approvals;
private Panel approvalPanel;
private NpTextArea message;
private FlowPanel draftsPanel;
@@ -83,9 +84,12 @@ public class PublishCommentScreen extends AccountScreen implements
addStyleName(Gerrit.RESOURCES.css().publishCommentsScreen());
approvalButtons = new ArrayList<ValueRadioButton>();
- descBlock = new ChangeDescriptionBlock();
+ descBlock = new ChangeDescriptionBlock(null);
add(descBlock);
+ approvals = new ApprovalTable();
+ add(approvals);
+
final FormPanel form = new FormPanel();
final FlowPanel body = new FlowPanel();
form.setWidget(body);
@@ -270,10 +274,15 @@ public class PublishCommentScreen extends AccountScreen implements
private void display(final PatchSetPublishDetail r) {
setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
patchSetId.get()));
- descBlock.display(r.getChange(), r.getPatchSetInfo(), r.getAccounts());
+ descBlock.display(r.getChange(), null, r.getPatchSetInfo(), r.getAccounts());
if (r.getChange().getStatus().isOpen()) {
initApprovals(r, approvalPanel);
+
+ approvals.setAccountInfoCache(r.getAccounts());
+ approvals.display(r);
+ } else {
+ approvals.setVisible(false);
}
if (lastState != null && patchSetId.equals(lastState.patchSetId)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index cf9c526874..b94fcaef46 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -17,13 +17,11 @@ package com.google.gerrit.client.changes;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.client.KeyUtil;
-
public class QueryScreen extends PagedSingleListScreen implements
ChangeListScreen {
public static QueryScreen forQuery(String query) {
@@ -49,13 +47,15 @@ public class QueryScreen extends PagedSingleListScreen implements
}
@Override
- protected AsyncCallback<SingleListChangeInfo> loadCallback() {
- return new GerritCallback<SingleListChangeInfo>() {
- public final void onSuccess(final SingleListChangeInfo result) {
+ protected AsyncCallback<ChangeList> loadCallback() {
+ return new GerritCallback<ChangeList>() {
+ @Override
+ public final void onSuccess(ChangeList result) {
if (isAttached()) {
- if (result.getChanges().size() == 1 && isSingleQuery(query)) {
- final ChangeInfo c = result.getChanges().get(0);
- Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
+ if (result.size() == 1 && isSingleQuery(query)) {
+ ChangeInfo c = result.get(0);
+ Change.Id id = c.legacy_id();
+ Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
} else {
Gerrit.setQueryString(query);
display(result);
@@ -68,12 +68,12 @@ public class QueryScreen extends PagedSingleListScreen implements
@Override
protected void loadPrev() {
- Util.LIST_SVC.allQueryPrev(query, pos, pageSize, loadCallback());
+ ChangeList.prev(query, pageSize, pos, loadCallback());
}
@Override
protected void loadNext() {
- Util.LIST_SVC.allQueryNext(query, pos, pageSize, loadCallback());
+ ChangeList.next(query, pageSize, pos, loadCallback());
}
private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
deleted file mode 100644
index d7624a6fd8..0000000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// 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.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.client.Change;
-
-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.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class StarCache implements HasValueChangeHandlers<Boolean> {
- public class KeyCommand extends NeedsSignInKeyCommand {
- public KeyCommand(int mask, char key, String help) {
- super(mask, key, help);
- }
-
- @Override
- public void onKeyPress(final KeyPressEvent event) {
- StarCache.this.toggleStar();
- }
- }
-
- ChangeCache cache;
-
- private HandlerManager manager = new HandlerManager(this);
-
- public StarCache(final Change.Id chg) {
- cache = ChangeCache.get(chg);
- }
-
- public boolean get() {
- ChangeDetail detail = cache.getChangeDetailCache().get();
- if (detail != null) {
- return detail.isStarred();
- }
- ChangeInfo info = cache.getChangeInfoCache().get();
- if (info != null) {
- return info.isStarred();
- }
- return false;
- }
-
- public void set(final boolean s) {
- if (Gerrit.isSignedIn() && s != get()) {
- final ToggleStarRequest req = new ToggleStarRequest();
- req.toggle(cache.getChangeId(), s);
-
- Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
- public void onSuccess(final VoidResult result) {
- setStarred(s);
- fireEvent(new ValueChangeEvent<Boolean>(s){});
- }
- });
- }
- }
-
- private void setStarred(final boolean s) {
- ChangeDetail detail = cache.getChangeDetailCache().get();
- if (detail != null) {
- detail.setStarred(s);
- }
- ChangeInfo info = cache.getChangeInfoCache().get();
- if (info != null) {
- info.setStarred(s);
- }
- }
-
- public void toggleStar() {
- set(!get());
- }
-
- @SuppressWarnings("unchecked")
- public Image createStar() {
- final Image star = new Image(getResource());
- star.setVisible(Gerrit.isSignedIn());
-
- star.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(final ClickEvent event) {
- StarCache.this.toggleStar();
- }
- });
-
- @SuppressWarnings("rawtypes")
- ValueChangeHandler starUpdater = new ValueChangeHandler() {
- @Override
- public void onValueChange(ValueChangeEvent event) {
- star.setResource(StarCache.this.getResource());
- }
- };
-
- cache.getChangeDetailCache().addValueChangeHandler(starUpdater);
- cache.getChangeInfoCache().addValueChangeHandler(starUpdater);
-
- this.addValueChangeHandler(starUpdater);
-
- return star;
- }
-
- private ImageResource getResource() {
- return get() ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
- }
-
- public void fireEvent(GwtEvent<?> event) {
- manager.fireEvent(event);
- }
-
- public HandlerRegistration addValueChangeHandler(
- ValueChangeHandler<Boolean> handler) {
- return manager.addHandler(ValueChangeEvent.getType(), handler);
- }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
new file mode 100644
index 0000000000..8b5aa1cd4a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -0,0 +1,216 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.reviewdb.client.Change;
+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.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/** Supports the star icon displayed on changes and tracking the status. */
+public class StarredChanges {
+ private static final EventBus eventBus = new SimpleEventBus();
+ private static final Event.Type<ChangeStarHandler> TYPE =
+ new Event.Type<ChangeStarHandler>();
+
+ /** Handler that can receive notifications of a change's starred status. */
+ public static interface ChangeStarHandler {
+ public void onChangeStar(ChangeStarEvent event);
+ }
+
+ /** Event fired when a star changes status. The new status is reported. */
+ public static class ChangeStarEvent extends Event<ChangeStarHandler> {
+ private boolean starred;
+
+ public ChangeStarEvent(Change.Id source, boolean starred) {
+ setSource(source);
+ this.starred = starred;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ @Override
+ public Type<ChangeStarHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(ChangeStarHandler handler) {
+ handler.onChangeStar(this);
+ }
+ }
+
+ /**
+ * Create a star icon for the given change, and current status. Returns null
+ * if the user is not signed in and cannot support starred changes.
+ */
+ public static Icon createIcon(Change.Id source, boolean starred) {
+ return Gerrit.isSignedIn() ? new Icon(source, starred) : null;
+ }
+
+ /** Make a key command that toggles the star for a change. */
+ public static KeyCommand newKeyCommand(final Icon icon) {
+ return new KeyCommand(0, 's', Util.C.changeTableStar()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ icon.toggleStar();
+ }
+ };
+ }
+
+ /** Add a handler to listen for starred status to change. */
+ public static HandlerRegistration addHandler(
+ Change.Id source,
+ ChangeStarHandler handler) {
+ return eventBus.addHandlerToSource(TYPE, source, handler);
+ }
+
+ /**
+ * Broadcast the current starred value of a change to UI widgets. This does
+ * not RPC to the server and does not alter the starred status of a change.
+ */
+ public static void fireChangeStarEvent(Change.Id id, boolean starred) {
+ eventBus.fireEventFromSource(
+ new ChangeStarEvent(id, starred),
+ id);
+ }
+
+ /**
+ * Set the starred status of a change. This method broadcasts to all
+ * interested UI widgets and sends an RPC to the server to record the
+ * updated status.
+ */
+ public static void toggleStar(
+ final Change.Id changeId,
+ final boolean newValue) {
+ if (next == null) {
+ next = new ToggleStarRequest();
+ }
+ next.toggle(changeId, newValue);
+ fireChangeStarEvent(changeId, newValue);
+ if (!busy) {
+ start();
+ }
+ }
+
+ private static ToggleStarRequest next;
+ private static boolean busy;
+
+ private static void start() {
+ final ToggleStarRequest req = next;
+ next = null;
+ busy = true;
+
+ Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+ @Override
+ public void onSuccess(VoidResult result) {
+ if (next != null) {
+ start();
+ } else {
+ busy = false;
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ rollback(req);
+ if (next != null) {
+ rollback(next);
+ next = null;
+ }
+ busy = false;
+ super.onFailure(caught);
+ }
+ });
+ }
+
+ private static void rollback(ToggleStarRequest req) {
+ if (req.getAddSet() != null) {
+ for (Change.Id id : req.getAddSet()) {
+ fireChangeStarEvent(id, false);
+ }
+ }
+ if (req.getRemoveSet() != null) {
+ for (Change.Id id : req.getRemoveSet()) {
+ fireChangeStarEvent(id, true);
+ }
+ }
+ }
+
+ public static class Icon extends Image
+ implements ChangeStarHandler, ClickHandler {
+ private final Change.Id changeId;
+ private boolean starred;
+ private HandlerRegistration handler;
+
+ Icon(Change.Id changeId, boolean starred) {
+ super(resource(starred));
+ this.changeId = changeId;
+ this.starred = starred;
+ addClickHandler(this);
+ }
+
+ /**
+ * Toggles the state of the star, as if the user clicked on the image. This
+ * will broadcast the new star status to all interested UI widgets, and RPC
+ * to the server to store the changed value.
+ */
+ public void toggleStar() {
+ StarredChanges.toggleStar(changeId, !starred);
+ }
+
+ @Override
+ protected void onLoad() {
+ handler = StarredChanges.addHandler(changeId, this);
+ }
+
+ @Override
+ protected void onUnload() {
+ handler.removeHandler();
+ handler = null;
+ }
+
+ @Override
+ public void onChangeStar(ChangeStarEvent event) {
+ setResource(resource(event.isStarred()));
+ starred = event.isStarred();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ toggleStar();
+ }
+
+ private static ImageResource resource(boolean starred) {
+ return starred ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
+ }
+ }
+
+ private StarredChanges() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index e84cac8eb2..590ad876a3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -30,6 +30,10 @@ public class Util {
public static final ChangeListService LIST_SVC;
public static final ChangeManageService MANAGE_SVC;
+ private static final int SUBJECT_MAX_LENGTH = 80;
+ private static final String SUBJECT_CROP_APPENDIX = "...";
+ private static final int SUBJECT_CROP_RANGE = 10;
+
static {
DETAIL_SVC = GWT.create(ChangeDetailService.class);
JsonUtil.bind(DETAIL_SVC, "rpc/ChangeDetailService");
@@ -60,4 +64,40 @@ public class Util {
return status.name();
}
}
+
+ /**
+ * Crops the given change subject if needed so that it has at most
+ * {@link #SUBJECT_MAX_LENGTH} characters.
+ *
+ * If the given subject is not longer than {@link #SUBJECT_MAX_LENGTH}
+ * characters it is returned unchanged.
+ *
+ * If the length of the given subject exceeds {@link #SUBJECT_MAX_LENGTH}
+ * characters it is cropped. In this case {@link #SUBJECT_CROP_APPENDIX} is
+ * appended to the cropped subject, the cropped subject including the appendix
+ * has at most {@link #SUBJECT_MAX_LENGTH} characters.
+ *
+ * If cropping is needed, the subject will be cropped after the last space
+ * character that is found within the last {@link #SUBJECT_CROP_RANGE}
+ * characters of the potentially visible characters. If no such space is
+ * found, the subject will be cropped so that the cropped subject including
+ * the appendix has exactly {@link #SUBJECT_MAX_LENGTH} characters.
+ *
+ * @return the subject, cropped if needed
+ */
+ @SuppressWarnings("deprecation")
+ public static String cropSubject(final String subject) {
+ if (subject.length() > SUBJECT_MAX_LENGTH) {
+ final int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
+ for (int cropPosition = maxLength; cropPosition > maxLength - SUBJECT_CROP_RANGE; cropPosition--) {
+ // Character.isWhitespace(char) can't be used because this method is not supported by GWT,
+ // see https://developers.google.com/web-toolkit/doc/1.6/RefJreEmulation#Package_java_lang
+ if (Character.isSpace(subject.charAt(cropPosition - 1))) {
+ return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
+ }
+ }
+ return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
+ }
+ return subject;
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png
new file mode 100644
index 0000000000..22ff495710
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.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 1081e47556..7512d8cf11 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
@@ -28,6 +28,7 @@
@external .gwt-InlineHyperlink;
@external .gwt-RadioButton;
+@external .searchPanel;
@external .smallHeading;
@external .wdi;
@external .wdd;
@@ -43,7 +44,9 @@
@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
@eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
+@eval changeTableOutdatedColor com.google.gerrit.client.Gerrit.getTheme().changeTableOutdatedColor;
+@eval tableOddRowColor com.google.gerrit.client.Gerrit.getTheme().tableOddRowColor;
+@eval tableEvenRowColor com.google.gerrit.client.Gerrit.getTheme().tableEvenRowColor;
@sprite .greenCheckClass {
gwt-image: "greenCheck";
@@ -375,6 +378,18 @@
width: 100%;
margin-top: 15px;
}
+.errorDialogText {
+ font-size: 15px;
+ font-family: verdana;
+}
+.errorDialog a,
+.errorDialog a:visited,
+.errorDialog a:hover {
+ color: white;
+ font-weight: bold;
+ font-size: 15px;
+ font-family: verdana;
+}
/** Screen **/
@@ -389,6 +404,9 @@
overflow: hidden;
}
+.screenNoHeader {
+ margin-top: 5px;
+}
/** ChangeTable **/
.changeTable {
@@ -396,8 +414,16 @@
border-spacing: 0;
}
+.changeTable tr:nth-child\(even\) {
+ background: tableEvenRowColor;
+}
+
+.changeTable tr:nth-child\(odd\) {
+ background: tableOddRowColor;
+}
+
.changeTable .outdated {
- background: red;
+ background: changeTableOutdatedColor !important;
}
.changeTable .iconCell {
@@ -467,7 +493,6 @@
.accountDashboard.changeTable tr {
color: #444444;
- background: #F6F6F6;
}
.accountDashboard.changeTable tr a {
color: #444444;
@@ -477,13 +502,12 @@
.accountDashboard.changeTable .needsReview a {
font-weight: bold;
color: textColor;
- background: backgroundColor;
}
.changeTable .activeRow,
.accountDashboard.changeTable .activeRow,
.accountDashboard.changeTable .activeRow a {
- background: selectionColor;
+ background: selectionColor !important;
}
.changeTable .cID {
@@ -662,11 +686,17 @@
white-space: pre;
width: 3.5em;
text-align: right;
- border-right: thin solid #b0bdcc;
padding-right: 0.2em;
background: white;
border-bottom: 1px solid white;
}
+.lineNumber.rightmost {
+ border-left: thin solid #b0bdcc;
+}
+.lineNumber a {
+ color: #888;
+ text-decoration: none;
+}
.patchContentTable td.fileColumnHeader {
background: trimColor;
font-family: norm-font;
@@ -689,6 +719,7 @@
padding-left: 0;
padding-right: 0;
white-space: pre;
+ border-left: thin solid #b0bdcc;
}
.fileLineNone {
background: #eeeeee;
@@ -723,11 +754,21 @@
font-family: norm-font;
text-align: center;
font-style: italic;
- background: lightblue;
+ background: #def;
+ color: grey;
}
.patchContentTable .skipLine div {
display: inline;
}
+.patchContentTable a.skipLine {
+ color: grey;
+ text-decoration: none;
+}
+.patchContentTable a:hover.skipLine {
+ background: white;
+ color: #00A;
+ text-decoration: underline;
+}
.patchContentTable .activeRow .iconCell,
.patchContentTable .activeRow .lineNumber {
@@ -882,15 +923,6 @@
font-weight: bold;
}
-.infoBlock td.permalink {
- border-right: 1px none;
- border-bottom: 1px none;
- text-align: right;
-}
-.infoBlock td.permalink div div {
- display: inline;
-}
-
.infoBlock td.useridentity {
white-space: nowrap;
}
@@ -923,7 +955,7 @@
margin-top: 5px;
}
-.changeScreen .approvalTable {
+.approvalTable {
margin-top: 1em;
margin-bottom: 1em;
}
@@ -1019,6 +1051,10 @@ a:hover.downloadLink {
display: table;
}
+.sideBySideScreenSideBySideTable .fileLine {
+ width: 50%;
+}
+
.sideBySideScreenLinkTable {
width: 100%;
}
@@ -1082,6 +1118,9 @@ a:hover.downloadLink {
.accountInfoBlock {
margin-bottom: 10px;
}
+.accountInfoBlock .gwt-Button {
+ margin-left: 10px;
+}
.accountContactPrivacyDetails {
margin-left: 10px;
margin-top: 5px;
@@ -1110,7 +1149,7 @@ a:hover.downloadLink {
padding: 5px 5px 5px 5px;
}
-.createProjectLink {
+.createGroupLink {
margin-bottom: 10px;
}
@@ -1354,3 +1393,7 @@ a:hover.downloadLink {
font-style: italic;
padding: 2px 6px 1px;
}
+
+/** PluginListScreen **/
+.pluginsTable {
+}
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 8282caa478..24a2ae55c6 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
@@ -516,22 +516,6 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object>
Gerrit.RESOURCES.css().iconCell());
}
- protected void addStyle(final int row, final int col, final String style) {
- table.getCellFormatter().addStyleName(row, col, style);
- }
-
- protected void removeRow(final int row) {
- table.removeRow(row);
- }
-
- protected void setHtml(final int row, final int col, final String html) {
- table.setHTML(row, col, html);
- }
-
- protected void setWidget(final int row, final int col, final Widget widget) {
- table.setWidget(row, col, widget);
- }
-
@Override
protected void onOpenRow(final int row) {
final Object item = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index fd347292fc..4801e65782 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -31,6 +31,7 @@ public interface PatchConstants extends Constants {
String patchHeaderPatchSet();
String patchHeaderOld();
String patchHeaderNew();
+ String patchSet();
String patchHistoryTitle();
String disabledOnLargeFiles();
@@ -47,6 +48,9 @@ public interface PatchConstants extends Constants {
String fileList();
String expandComment();
+ String toggleReviewed();
+ String markAsReviewedAndGoToNext();
+
String commentEditorSet();
String commentInsert();
String commentSaveDraft();
@@ -60,7 +64,8 @@ public interface PatchConstants extends Constants {
String previousFileHelp();
String nextFileHelp();
- String reviewed();
+ String reviewedAnd();
+ String next();
String download();
String buttonReplyDone();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 6a1dbc1ddc..11823acd76 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -15,6 +15,7 @@ patchHeaderPatchSet = Patch Set
patchHeaderOld = Old Version
patchHeaderNew = New Version
patchHistoryTitle = Patch History
+patchSet = Patch Set
disabledOnLargeFiles = Disabled on very large source files.
intralineFailure = Intraline difference not available due to server error.
illegalNumberOfColumns = The number of columns cannot be zero or negative
@@ -29,6 +30,9 @@ commentNext = Next comment
fileList = Browse files in patch set
expandComment = Expand or collapse comment
+toggleReviewed = Toggle the reviewed flag
+markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
+
commentEditorSet = Comment Editing
commentInsert = Create a new inline comment
commentSaveDraft = Save draft comment
@@ -42,11 +46,12 @@ whitespaceIGNORE_ALL_SPACE=All
previousFileHelp = Previous file
nextFileHelp = Next file
-reviewed = Reviewed
-download = (Download)
+reviewedAnd = Reviewed &
+next = next
+download = Download
fileTypeSymlink = Type: Symbolic Link
fileTypeGitlink = Type: Git Commit in Subproject
-patchSkipRegionStart = (... skipping
-patchSkipRegionEnd = common lines ...)
+patchSkipRegionStart = ... skipped
+patchSkipRegionEnd = common lines ...
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
index 52dcba2b34..2d01e24714 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
@@ -1,4 +1,3 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
-
+expandBefore = +{0}&#x21e7;
+expandAfter = +{0}&#x21e9;
draftSaved = Draft saved at {0,time,short}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
index 58c4f6c740..2f3c20a69a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
@@ -1,2 +1,2 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
+expandBefore = +{0}&#x21e7;
+expandAfter = +{0}&#x21e9;
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 ffc896084e..4f8731ca1f 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
@@ -20,9 +20,12 @@ import com.google.gerrit.client.Gerrit;
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.PatchTable.PatchValidator;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.data.PatchScript;
@@ -34,17 +37,22 @@ import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+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.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
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.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.common.VoidResult;
public abstract class PatchScreen extends Screen implements
@@ -102,10 +110,13 @@ public abstract class PatchScreen extends Screen implements
protected PatchScriptSettingsPanel settingsPanel;
protected TopView topView;
- private CheckBox reviewed;
+ private CheckBox reviewedCheckBox;
+ private FlowPanel reviewedPanel;
+ private InlineHyperlink reviewedLink;
private HistoryTable historyTable;
private FlowPanel topPanel;
private FlowPanel contentPanel;
+ private PatchTableHeader header;
private Label noDifference;
private AbstractPatchContentTable contentTable;
private CommitMessageBlock commitMessageBlock;
@@ -121,7 +132,9 @@ public abstract class PatchScreen extends Screen implements
/** Keys that cause an action on this screen */
private KeyCommandSet keysNavigation;
+ private KeyCommandSet keysAction;
private HandlerRegistration regNavigation;
+ private HandlerRegistration regAction;
private boolean intralineFailure;
/**
@@ -143,15 +156,6 @@ public abstract class PatchScreen extends Screen implements
idSideB = id.getParentKey();
this.patchIndex = patchIndex;
- reviewed = new CheckBox(Util.C.reviewed());
- reviewed.addValueChangeHandler(
- new ValueChangeHandler<Boolean>() {
- @Override
- public void onValueChange(ValueChangeEvent<Boolean> event) {
- setReviewedByCurrentUser(event.getValue());
- }
- });
-
prefs = fileList != null ? fileList.getPreferences() :
new ListenableAccountDiffPreference();
if (Gerrit.isSignedIn()) {
@@ -165,9 +169,63 @@ public abstract class PatchScreen extends Screen implements
}
});
+ reviewedPanel = new FlowPanel();
settingsPanel = new PatchScriptSettingsPanel(prefs);
}
+ private void populateReviewedPanel(){
+ reviewedPanel.clear();
+
+ reviewedCheckBox = new CheckBox(PatchUtil.C.reviewedAnd() + " ");
+ reviewedCheckBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+ @Override
+ public void onValueChange(ValueChangeEvent<Boolean> event) {
+ setReviewedByCurrentUser(event.getValue());
+ }
+ });
+
+ reviewedPanel.add(reviewedCheckBox);
+ reviewedPanel.add(getReviewedAnchor());
+ }
+
+ private Anchor getReviewedAnchor() {
+ SafeHtmlBuilder text = new SafeHtmlBuilder();
+ text.append(PatchUtil.C.next());
+ text.append(SafeHtml.asis(Util.C.nextPatchLinkIcon()));
+
+ Anchor reviewedAnchor = new Anchor("");
+ SafeHtml.set(reviewedAnchor, text);
+
+ final PatchValidator unreviewedValidator = new PatchValidator() {
+ public boolean isValid(Patch patch) {
+ return !patch.isReviewedByCurrentUser();
+ }
+ };
+
+ int nextUnreviewedPatchIndex =
+ fileList.getNextPatch(patchIndex, true, unreviewedValidator,
+ fileList.PREFERENCE_VALIDATOR);
+
+ if (nextUnreviewedPatchIndex > -1) {
+ // Create invisible patch link to change page
+ reviewedLink =
+ fileList.createLink(nextUnreviewedPatchIndex, getPatchScreenType(),
+ null, null);
+ reviewedLink.setText("");
+ } else {
+ reviewedLink = new ChangeLink("", patchKey.getParentKey());
+ }
+ reviewedAnchor.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ setReviewedByCurrentUser(true);
+ reviewedLink.go();
+ }
+ });
+
+ return reviewedAnchor;
+ }
+
@Override
public void notifyDraftDelta(int delta) {
lastScript = null;
@@ -180,9 +238,9 @@ public abstract class PatchScreen extends Screen implements
private void update(AccountDiffPreference dp) {
// Did the user just turn on auto-review?
- if (!reviewed.getValue() && prefs.getOld().isManualReview()
+ if (!reviewedCheckBox.getValue() && prefs.getOld().isManualReview()
&& !dp.isManualReview()) {
- reviewed.setValue(true);
+ reviewedCheckBox.setValue(true);
setReviewedByCurrentUser(true);
}
@@ -236,13 +294,21 @@ public abstract class PatchScreen extends Screen implements
super.onInitUI();
if (Gerrit.isSignedIn()) {
- setTitleFarEast(reviewed);
+ setTitleFarEast(reviewedPanel);
}
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
keysNavigation.add(new UpToChangeCommand(patchKey.getParentKey(), 0, 'u'));
keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
+ if (Gerrit.isSignedIn()) {
+ keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+ keysAction
+ .add(new ToggleReviewedCmd(0, 'm', PatchUtil.C.toggleReviewed()));
+ keysAction.add(new MarkAsReviewedAndGoToNextCmd(0, 'M', PatchUtil.C
+ .markAsReviewedAndGoToNext()));
+ }
+
historyTable = new HistoryTable(this);
commitMessageBlock = new CommitMessageBlock();
@@ -250,6 +316,8 @@ public abstract class PatchScreen extends Screen implements
topPanel = new FlowPanel();
add(topPanel);
+ header = new PatchTableHeader(getPatchScreenType());
+
noDifference = new Label(PatchUtil.C.noDifference());
noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
noDifference.setVisible(false);
@@ -264,6 +332,7 @@ public abstract class PatchScreen extends Screen implements
contentPanel = new FlowPanel();
contentPanel.setStyleName(Gerrit.RESOURCES.css()
.sideBySideScreenSideBySideTable());
+ contentPanel.add(header);
contentPanel.add(noDifference);
contentPanel.add(contentTable);
add(contentPanel);
@@ -297,6 +366,7 @@ public abstract class PatchScreen extends Screen implements
@Override
protected void onLoad() {
super.onLoad();
+
if (patchSetDetail == null) {
Util.DETAIL_SVC.patchSetDetail(idSideB,
new GerritCallback<PatchSetDetail>() {
@@ -322,6 +392,10 @@ public abstract class PatchScreen extends Screen implements
regNavigation.removeHandler();
regNavigation = null;
}
+ if (regAction != null) {
+ regAction.removeHandler();
+ regAction = null;
+ }
super.onUnload();
}
@@ -334,6 +408,13 @@ public abstract class PatchScreen extends Screen implements
regNavigation = null;
}
regNavigation = GlobalKey.add(this, keysNavigation);
+ if (regAction != null) {
+ regAction.removeHandler();
+ regAction = null;
+ }
+ if (keysAction != null) {
+ regAction = GlobalKey.add(this, keysAction);
+ }
}
protected abstract AbstractPatchContentTable createContentTable();
@@ -368,6 +449,10 @@ public abstract class PatchScreen extends Screen implements
final int rpcseq = ++rpcSequence;
lastScript = null;
settingsPanel.setEnabled(false);
+ populateReviewedPanel();
+ if (isFirst && fileList != null) {
+ fileList.movePointerTo(patchKey);
+ }
PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
@Override
@@ -435,6 +520,8 @@ public abstract class PatchScreen extends Screen implements
setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
}
+ header.display(patchSetDetail, script, patchKey, idSideA, idSideB);
+
if (hasDifferences) {
contentTable.display(patchKey, idSideA, idSideB, script);
contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
@@ -464,7 +551,7 @@ public abstract class PatchScreen extends Screen implements
}
}
}
- reviewed.setValue(isReviewed);
+ reviewedCheckBox.setValue(isReviewed);
}
intralineFailure = isFirst && script.hasIntralineFailure();
@@ -526,4 +613,31 @@ public abstract class PatchScreen extends Screen implements
p.open();
}
}
+
+ public class ToggleReviewedCmd extends KeyCommand {
+ public ToggleReviewedCmd(int mask, int key, String help) {
+ super(mask, key, help);
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ final boolean isReviewed = !reviewedCheckBox.getValue();
+ reviewedCheckBox.setValue(isReviewed);
+ setReviewedByCurrentUser(isReviewed);
+ }
+ }
+
+ public class MarkAsReviewedAndGoToNextCmd extends KeyCommand {
+ public MarkAsReviewedAndGoToNextCmd(int mask, int key, String help) {
+ super(mask, key, help);
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ if (reviewedLink != null) {
+ setReviewedByCurrentUser(true);
+ reviewedLink.go();
+ }
+ }
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 871eb2bd2d..a689259a5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -76,6 +76,9 @@ public class PatchScriptSettingsPanel extends Composite implements
CheckBox whitespaceErrors;
@UiField
+ CheckBox showLineEndings;
+
+ @UiField
CheckBox showTabs;
@UiField
@@ -210,6 +213,7 @@ public class PatchScriptSettingsPanel extends Composite implements
colWidth.setIntValue(dp.getLineLength());
intralineDifference.setValue(dp.isIntralineDifference());
whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
+ showLineEndings.setValue(dp.isShowLineEndings());
showTabs.setValue(dp.isShowTabs());
skipDeleted.setValue(dp.isSkipDeleted());
skipUncommented.setValue(dp.isSkipUncommented());
@@ -242,6 +246,7 @@ public class PatchScriptSettingsPanel extends Composite implements
dp.setSyntaxHighlighting(syntaxHighlighting.getValue());
dp.setIntralineDifference(intralineDifference.getValue());
dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
+ dp.setShowLineEndings(showLineEndings.getValue());
dp.setShowTabs(showTabs.getValue());
dp.setSkipDeleted(skipDeleted.getValue());
dp.setSkipUncommented(skipUncommented.getValue());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index 2c7afffefb..586b7672e8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -108,8 +108,8 @@ limitations under the License.
</g:CheckBox>
<br/>
<g:CheckBox
- ui:field='showTabs'
- text='Show Tabs'
+ ui:field='showLineEndings'
+ text='Show Line Endings'
tabIndex='8'>
<ui:attribute name='text'/>
</g:CheckBox>
@@ -117,15 +117,15 @@ limitations under the License.
<td rowspan='2'>
<g:CheckBox
- ui:field='expandAllComments'
- text='Expand All Comments'
+ ui:field='showTabs'
+ text='Show Tabs'
tabIndex='9'>
<ui:attribute name='text'/>
</g:CheckBox>
<br/>
<g:CheckBox
- ui:field='retainHeader'
- text='Retain Header On File Switch'
+ ui:field='expandAllComments'
+ text='Expand All Comments'
tabIndex='10'>
<ui:attribute name='text'/>
</g:CheckBox>
@@ -133,15 +133,15 @@ limitations under the License.
<td rowspan='2'>
<g:CheckBox
- ui:field='skipUncommented'
- text='Skip Uncommented Files'
+ ui:field='retainHeader'
+ text='Retain Header On File Switch'
tabIndex='11'>
<ui:attribute name='text'/>
</g:CheckBox>
<br/>
<g:CheckBox
- ui:field='skipDeleted'
- text='Skip Deleted Files'
+ ui:field='skipUncommented'
+ text='Skip Uncommented Files'
tabIndex='12'>
<ui:attribute name='text'/>
</g:CheckBox>
@@ -149,9 +149,16 @@ limitations under the License.
<td valign='bottom' rowspan='2'>
<g:CheckBox
+ ui:field='skipDeleted'
+ text='Skip Deleted Files'
+ tabIndex='13'>
+ <ui:attribute name='text'/>
+ </g:CheckBox>
+ <br/>
+ <g:CheckBox
ui:field='manualReview'
text='Manual Review'
- tabIndex='13'>
+ tabIndex='14'>
<ui:attribute name='text'/>
</g:CheckBox>
</td>
@@ -162,14 +169,14 @@ limitations under the License.
ui:field='update'
text='Update'
styleName='{style.updateButton}'
- tabIndex='14'>
+ tabIndex='15'>
<ui:attribute name='text'/>
</g:Button>
<g:Button
ui:field='save'
text='Save'
styleName='{style.updateButton}'
- tabIndex='15'>
+ tabIndex='16'>
<ui:attribute name='text'/>
</g:Button>
</td>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
new file mode 100644
index 0000000000..afaf7fdbb2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -0,0 +1,189 @@
+// 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.client.patches;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PatchSetSelectBox extends Composite {
+ interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {
+ }
+
+ private static Binder uiBinder = GWT.create(Binder.class);
+
+ interface BoxStyle extends CssResource {
+ String selected();
+
+ String hidden();
+
+ String downloadLink();
+ }
+
+ public enum Side {
+ A, B
+ }
+
+ PatchScript script;
+ Patch.Key patchKey;
+ PatchSet.Id idSideA;
+ PatchSet.Id idSideB;
+ PatchSet.Id idActive;
+ Side side;
+ PatchScreen.Type screenType;
+ Map<Integer, Anchor> links;
+
+ @UiField
+ HTMLPanel linkPanel;
+
+ @UiField
+ BoxStyle style;
+
+ @UiField
+ DivElement sideMarker;
+
+ public PatchSetSelectBox(Side side, final PatchScreen.Type type) {
+ this.side = side;
+ this.screenType = type;
+
+ initWidget(uiBinder.createAndBindUi(this));
+ }
+
+ public void display(final PatchSetDetail detail, final PatchScript script, Patch.Key key,
+ PatchSet.Id idSideA, PatchSet.Id idSideB) {
+ this.script = script;
+ this.patchKey = key;
+ this.idSideA = idSideA;
+ this.idSideB = idSideB;
+ this.idActive = (side == Side.A) ? idSideA : idSideB;
+ this.links = new HashMap<Integer, Anchor>();
+
+ linkPanel.clear();
+
+ if (screenType == PatchScreen.Type.UNIFIED) {
+ sideMarker.setInnerText((side == Side.A) ? "(-)" : "(+)");
+ } else {
+ sideMarker.getStyle().setDisplay(Display.NONE);
+ }
+
+ Anchor baseLink = null;
+ if (detail.getInfo().getParents().size() > 1) {
+ baseLink = createLink(PatchUtil.C.patchBaseAutoMerge(), null);
+ } else {
+ baseLink = createLink(PatchUtil.C.patchBase(), null);
+ }
+
+ links.put(0, baseLink);
+ if (screenType == PatchScreen.Type.UNIFIED || side == Side.A) {
+ linkPanel.add(baseLink);
+ }
+
+ if (side == Side.B) {
+ links.get(0).setStyleName(style.hidden());
+ }
+
+ for (Patch patch : script.getHistory()) {
+ PatchSet.Id psId = patch.getKey().getParentKey();
+ Anchor anchor = createLink(Integer.toString(psId.get()), psId);
+ links.put(psId.get(), anchor);
+ linkPanel.add(anchor);
+ }
+
+ if (idActive == null && side == Side.A) {
+ links.get(0).setStyleName(style.selected());
+ } else {
+ links.get(idActive.get()).setStyleName(style.selected());
+ }
+
+ Anchor downloadLink = createDownloadLink();
+ if (downloadLink != null) {
+ linkPanel.add(downloadLink);
+ }
+ }
+
+ private Anchor createLink(String label, final PatchSet.Id id) {
+ final Anchor anchor = new Anchor(label);
+ anchor.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (side == Side.A) {
+ idSideA = id;
+ } else {
+ idSideB = id;
+ }
+
+ Patch.Key keySideB = new Patch.Key(idSideB, patchKey.get());
+
+ switch (screenType) {
+ case SIDE_BY_SIDE:
+ Gerrit.display(Dispatcher.toPatchSideBySide(idSideA, keySideB));
+ break;
+ case UNIFIED:
+ Gerrit.display(Dispatcher.toPatchUnified(idSideA, keySideB));
+ break;
+ }
+ }
+
+ });
+
+ return anchor;
+ }
+
+ private Anchor createDownloadLink() {
+ boolean isCommitMessage = Patch.COMMIT_MSG.equals(script.getNewName());
+
+ if (isCommitMessage || (side == Side.A && 0 >= script.getA().size())
+ || (side == Side.B && 0 >= script.getB().size())) {
+ return null;
+ }
+
+ Patch.Key key =
+ (idSideA == null) ? patchKey : (new Patch.Key(idSideA, patchKey.get()));
+
+ String sideURL = (side == Side.A) ? "1" : "0";
+ final String base = GWT.getHostPageBaseURL() + "cat/";
+
+ Image image = new Image(Gerrit.RESOURCES.downloadIcon());
+
+ final Anchor anchor = new Anchor();
+ anchor.setHref(base + KeyUtil.encode(key.toString()) + "^" + sideURL);
+ anchor.setTitle(PatchUtil.C.download());
+ anchor.setStyleName(style.downloadLink());
+ DOM.insertBefore(anchor.getElement(), image.getElement(),
+ DOM.getFirstChild(anchor.getElement()));
+
+ return anchor;
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
new file mode 100644
index 0000000000..2fd183cefc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+ <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+ <ui:with field='cons' type='com.google.gerrit.client.patches.PatchConstants'/>
+ <ui:style type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
+ @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+ @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+ .wrapper {
+ width: 100%;
+ text-align: center;
+ font-size: 0; /* inline-block spacing fix */
+ }
+
+ .linkPanel {
+ display: inline-block;
+ }
+
+ .linkPanel > div {
+ display: inline-block;
+ float: left;
+ }
+
+ .linkPanel {
+ overflow: hidden; /* div clear fix */
+ font-size: 12px;
+ }
+
+ .linkPanel > a {
+ padding: 3px;
+ display: inline-block;
+ text-decoration: none;
+ float: left;
+ }
+
+ .patchSetLabel {
+ font-weight: bold;
+ float: left;
+ padding: 3px;
+ }
+
+ .sideMarker {
+ padding: 3px;
+ }
+
+ .downloadLink {
+ float: left;
+ padding: 1px !important;
+ margin-left: 3px;
+ }
+
+ .downloadLink > a {
+ text-size: 0;
+ }
+
+ .selected {
+ font-weight: bold;
+ background-color: selectionColor;
+ }
+
+ .sideMarker {
+ font-family: monospace;
+ float: left;
+ }
+
+ .hidden {
+ visibility: hidden;
+ }
+ </ui:style>
+
+ <g:HTMLPanel styleName='wrapper'>
+ <g:HTMLPanel styleName='{style.linkPanel}' ui:field='linkPanel'>
+ <div class='{style.patchSetLabel}'><ui:text from="{cons.patchSet}" /></div>
+ <div class='{style.sideMarker}' ui:field='sideMarker'></div>
+ </g:HTMLPanel>
+ </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
new file mode 100644
index 0000000000..3dd8908bba
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
@@ -0,0 +1,71 @@
+// 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.client.patches;
+
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiTemplate;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+public class PatchTableHeader extends Composite {
+
+ @UiTemplate("PatchTableHeaderSideBySide.ui.xml")
+ interface SideBySideBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
+ }
+
+ @UiTemplate("PatchTableHeaderUnified.ui.xml")
+ interface UnifiedBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
+ }
+
+ private static SideBySideBinder uiBinderS = GWT.create(SideBySideBinder.class);
+ private static UnifiedBinder uiBinderU = GWT.create(UnifiedBinder.class);
+
+ @UiField
+ SimplePanel sideAPanel;
+
+ @UiField
+ SimplePanel sideBPanel;
+
+ PatchSetSelectBox listA;
+ PatchSetSelectBox listB;
+
+ public PatchTableHeader(PatchScreen.Type type) {
+ listA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type);
+ listB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type);
+
+ if (type == PatchScreen.Type.SIDE_BY_SIDE) {
+ initWidget(uiBinderS.createAndBindUi(this));
+ } else {
+ initWidget(uiBinderU.createAndBindUi(this));
+ }
+
+ sideAPanel.add(listA);
+ sideBPanel.add(listB);
+ }
+
+
+ public void display(final PatchSetDetail detail, PatchScript script, final Patch.Key patchKey,
+ final PatchSet.Id idSideA, final PatchSet.Id idSideB) {
+ listA.display(detail, script, patchKey, idSideA, idSideB);
+ listB.display(detail, script, patchKey, idSideA, idSideB);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml
new file mode 100644
index 0000000000..d6fd717fab
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+ <ui:style>
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+ .wrapper {
+ width: 100%;
+ background-color: trimColor;
+ overflow: hidden;
+ font-size: 0; /* inline-block spacing fix */
+ }
+
+ .wrapper .box {
+ width: 100%;
+ text-align: center;
+ }
+
+ .leftWrapper {
+ width: 50%;
+ float: left;
+ }
+
+ .rightWrapper {
+ width: 50%;
+ overflow: hidden;
+ }
+
+ .leftBox {
+ float:left;
+ }
+
+ .rightBox {
+ float: right;
+ }
+ </ui:style>
+
+ <g:HTMLPanel styleName="{style.wrapper}">
+ <div class='{style.leftWrapper}'>
+ <g:SimplePanel addStyleNames='{style.box} {style.leftBox}' ui:field='sideAPanel'/>
+ </div>
+ <div class='{style.rightWrapper}'>
+ <g:SimplePanel addStyleNames='{style.box} {style.rightBox}' ui:field='sideBPanel'/>
+ </div>
+ </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
new file mode 100644
index 0000000000..24acfa3e8e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+ xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+ <ui:style>
+ @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+ .wrapper {
+ width: 100%;
+ background-color: trimColor;
+ font-size: 0; /* inline-block spacing fix */
+ }
+
+ .wrapper .box {
+ width: 100%;
+ text-align: left;
+ margin-left: 3px;
+ }
+ </ui:style>
+
+ <g:HTMLPanel styleName="{style.wrapper}">
+ <g:SimplePanel addStyleNames='{style.box}' ui:field='sideAPanel'/>
+ <g:SimplePanel addStyleNames='{style.box}' ui:field='sideBPanel'/>
+ </g:HTMLPanel>
+</ui:UiBinder>
+
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 6379e23121..ec63a83121 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
@@ -25,21 +25,16 @@ import com.google.gerrit.common.data.PatchScript;
import com.google.gerrit.common.data.PatchScript.FileMode;
import com.google.gerrit.prettify.common.EditList;
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.Patch.ChangeType;
-import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
-
import org.eclipse.jgit.diff.Edit;
import java.util.ArrayList;
@@ -47,9 +42,8 @@ import java.util.Iterator;
import java.util.List;
public class SideBySideTable extends AbstractPatchContentTable {
- private static final int COL_A = 2;
- private static final int COL_B = 4;
-
+ private static final int A = 2;
+ private static final int B = 3;
private static final int NUM_ROWS_TO_EXPAND = 10;
private SparseHtmlFile a;
@@ -59,24 +53,17 @@ public class SideBySideTable extends AbstractPatchContentTable {
protected void onCellDoubleClick(final int row, int column) {
if (column > 0 && getRowItem(row) instanceof PatchLine) {
final PatchLine line = (PatchLine) getRowItem(row);
- final short file = (short) ((column - 1) / 2);
- if (column < (1 + file * 2 + 1)) {
- column++;
- }
- switch (file) {
- case 0:
- createCommentEditor(row + 1, column, line.getLineA(), file);
- break;
- case 1:
- createCommentEditor(row + 1, column, line.getLineB(), file);
- break;
+ if (column == 1 || column == A) {
+ createCommentEditor(row + 1, A, line.getLineA(), (short) 0);
+ } else if (column == B || column == 4) {
+ createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
}
}
}
@Override
protected void onCellSingleClick(int row, int column) {
- if (column == 1 || column == 3) {
+ if (column == 1 || column == 4) {
onCellDoubleClick(row, column);
}
}
@@ -84,7 +71,7 @@ public class SideBySideTable extends AbstractPatchContentTable {
@Override
protected void onInsertComment(final PatchLine line) {
final int row = getCurrentRow();
- createCommentEditor(row + 1, 4, line.getLineB(), (short) 1);
+ createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
}
@Override
@@ -97,10 +84,8 @@ public class SideBySideTable extends AbstractPatchContentTable {
script.getDiffPrefs().isIntralineDifference()
&& script.hasIntralineDifference();
- appendHeader(script, nc);
- lines.add(null);
-
- if(script.getFileModeA()!=FileMode.FILE||script.getFileModeB()!=FileMode.FILE){
+ if (script.getFileModeA() != FileMode.FILE
+ || script.getFileModeB() != FileMode.FILE) {
openLine(nc);
appendModeLine(nc, script.getFileModeA());
appendModeLine(nc, script.getFileModeB());
@@ -121,13 +106,14 @@ public class SideBySideTable extends AbstractPatchContentTable {
if (hunk.isContextLine()) {
openLine(nc);
final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
- appendLineText(nc, hunk.getCurA(), CONTEXT, ctx, false, false);
+ appendLineNumber(nc, hunk.getCurA(), false);
+ appendLineText(nc, CONTEXT, ctx, false, false);
if (ignoreWS && b.contains(hunk.getCurB())) {
- appendLineText(nc, hunk.getCurB(), CONTEXT, b, hunk.getCurB(),
- false);
+ appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
} else {
- appendLineText(nc, hunk.getCurB(), CONTEXT, ctx, false, false);
+ appendLineText(nc, CONTEXT, ctx, false, false);
}
+ appendLineNumber(nc, hunk.getCurB(), true);
closeLine(nc);
hunk.incBoth();
lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
@@ -140,21 +126,27 @@ public class SideBySideTable extends AbstractPatchContentTable {
openLine(nc);
if (del) {
- appendLineText(nc, hunk.getCurA(), DELETE, a, hunk.getCurA(), full);
+ appendLineNumber(nc, hunk.getCurA(), false);
+ appendLineText(nc, DELETE, a, hunk.getCurA(), full);
hunk.incA();
} else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+ appendLineNumber(nc, false);
appendLineNone(nc, DELETE);
} else {
+ appendLineNumber(nc, false);
appendLineNone(nc, CONTEXT);
}
if (ins) {
- appendLineText(nc, hunk.getCurB(), INSERT, b, hunk.getCurB(), full);
+ appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+ appendLineNumber(nc, hunk.getCurB(), true);
hunk.incB();
} else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
appendLineNone(nc, INSERT);
+ appendLineNumber(nc, true);
} else {
appendLineNone(nc, CONTEXT);
+ appendLineNumber(nc, true);
}
closeLine(nc);
@@ -229,13 +221,13 @@ public class SideBySideTable extends AbstractPatchContentTable {
final PatchLineComment ac = ai.next();
final PatchLineComment bc = bi.next();
insertRow(row);
- bindComment(row, COL_A, ac, !ai.hasNext(), expandComments);
- bindComment(row, COL_B, bc, !bi.hasNext(), expandComments);
+ bindComment(row, A, ac, !ai.hasNext(), expandComments);
+ bindComment(row, B, bc, !bi.hasNext(), expandComments);
row++;
}
- row = finish(ai, row, COL_A, expandComments);
- row = finish(bi, row, COL_B, expandComments);
+ row = finish(ai, row, A, expandComments);
+ row = finish(bi, row, B, expandComments);
} else {
row++;
}
@@ -246,10 +238,10 @@ public class SideBySideTable extends AbstractPatchContentTable {
protected void insertRow(final int row) {
super.insertRow(row);
final CellFormatter fmt = table.getCellFormatter();
- fmt.addStyleName(row, COL_A - 1, Gerrit.RESOURCES.css().lineNumber());
- fmt.addStyleName(row, COL_A, Gerrit.RESOURCES.css().diffText());
- fmt.addStyleName(row, COL_B - 1, Gerrit.RESOURCES.css().lineNumber());
- fmt.addStyleName(row, COL_B, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
+ fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
}
private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
@@ -262,65 +254,6 @@ public class SideBySideTable extends AbstractPatchContentTable {
return row;
}
- private void appendHeader(PatchScript script, final SafeHtmlBuilder m) {
- m.openTr();
-
- m.openTd();
- m.addStyleName(Gerrit.RESOURCES.css().iconCell());
- m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.closeTd();
-
- m.openTd();
- m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.addStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.closeTd();
-
- m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.setAttribute("width", "50%");
- if (script.getChangeType() == ChangeType.RENAMED
- || script.getChangeType() == ChangeType.COPIED) {
- m.append(script.getOldName());
- } else {
- m.append(PatchUtil.C.patchHeaderOld());
- }
- m.br();
- if (0 < script.getA().size()) {
- if (idSideA == null) {
- downloadLink(m, patchKey, "1");
- } else {
- downloadLink(m, new Patch.Key(idSideA, patchKey.get()), "0");
- }
- }
- m.closeTd();
-
- m.openTd();
- m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.addStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.closeTd();
-
- m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.setAttribute("width", "50%");
- m.append(PatchUtil.C.patchHeaderNew());
- m.br();
- if (0 < script.getB().size()) {
- downloadLink(m, new Patch.Key(idSideB, patchKey.get()), "0");
- }
- m.closeTd();
-
- m.closeTr();
- }
-
- private void downloadLink(final SafeHtmlBuilder m, final Patch.Key key,
- final String side) {
- final String base = GWT.getHostPageBaseURL() + "cat/";
- m.openAnchor();
- m.setAttribute("href", base + KeyUtil.encode(key.toString()) + "^" + side);
- m.append(PatchUtil.C.download());
- m.closeAnchor();
- }
-
private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) {
m.openTr();
@@ -336,21 +269,21 @@ public class SideBySideTable extends AbstractPatchContentTable {
m.closeTr();
}
- ClickHandler expandAllListener = new ClickHandler() {
+ private ClickHandler expandAllListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, 0);
}
};
- ClickHandler expandBeforeListener = new ClickHandler() {
+ private ClickHandler expandBeforeListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, NUM_ROWS_TO_EXPAND);
}
};
- ClickHandler expandAfterListener = new ClickHandler() {
+ private ClickHandler expandAfterListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, -NUM_ROWS_TO_EXPAND);
@@ -358,11 +291,11 @@ public class SideBySideTable extends AbstractPatchContentTable {
};
private void expand(ClickEvent event, final int numRows) {
- Cell cell = table.getCellForEvent(event);
- int row = cell.getRowIndex();
+ int row = table.getCellForEvent(event).getRowIndex();
if (!(getRowItem(row) instanceof SkippedLine)) {
return;
}
+
SkippedLine line = (SkippedLine) getRowItem(row);
int loopTo = numRows;
if (numRows == 0) {
@@ -374,29 +307,34 @@ public class SideBySideTable extends AbstractPatchContentTable {
if (numRows < 0) {
offset = 1;
}
+
+ CellFormatter fmt = table.getCellFormatter();
for (int i = 0 + offset; i < loopTo + offset; i++) {
- insertRow(row + i);
+ // The overridden version of insertRow adds some css classes we don't
+ // want.
+ super.insertRow(row + i);
+ table.getRowFormatter().setVerticalAlign(row + i,
+ HasVerticalAlignment.ALIGN_TOP);
int lineA = line.getStartA() + i;
int lineB = line.getStartB() + i;
if (numRows < 0) {
lineA = line.getStartA() + line.getSize() + numRows + i - offset;
lineB = line.getStartB() + line.getSize() + numRows + i - offset;
}
- setHtml(row + i, 1, "<a href=\"javascript:void(0)\">" + (lineA + 1)
- + "</a>");
- addStyle(row + i, 1, Gerrit.RESOURCES.css().lineNumber());
- setHtml(row + i, 2, a.getSafeHtmlLine(lineA).asString());
- addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLine());
- addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLineCONTEXT());
+ table.setHTML(row + i, A - 1, "<a href=\"javascript:;\">" + (lineA + 1) + "</a>");
+ fmt.addStyleName(row + i, A - 1, Gerrit.RESOURCES.css().lineNumber());
+
+ table.setHTML(row + i, A, a.getSafeHtmlLine(lineA).asString());
+ fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLine());
+ fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLineCONTEXT());
- setHtml(row + i, 3, "<a href=\"javascript:void(0)\">" + (lineB + 1)
- + "</a>");
- addStyle(row + i, 3, Gerrit.RESOURCES.css().lineNumber());
+ table.setHTML(row + i, B, b.getSafeHtmlLine(lineB).asString());
+ fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLine());
+ fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLineCONTEXT());
- setHtml(row + i, 4, b.getSafeHtmlLine(lineB).asString());
- addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLine());
- addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLineCONTEXT());
+ table.setHTML(row + i, B + 1, "<a href=\"javascript:;\">" + (lineB + 1) + "</a>");
+ fmt.addStyleName(row + i, B + 1, Gerrit.RESOURCES.css().lineNumber());
setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB));
}
@@ -408,34 +346,41 @@ public class SideBySideTable extends AbstractPatchContentTable {
line.reduceSize(-numRows);
createSkipLine(row, line);
} else {
- removeRow(row + loopTo);
+ table.removeRow(row + loopTo);
}
}
private void createSkipLine(int row, SkippedLine line) {
FlowPanel p = new FlowPanel();
- Label l1 = new Label(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+ InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+ InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
+
Anchor all = new Anchor(String.valueOf(line.getSize()));
- Label l2 = new Label(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
all.addClickHandler(expandAllListener);
+ all.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
if (line.getSize() > 30) {
- // We only show the expand before & after links if we skip more than
- // 30 lines.
- Anchor before = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
- before.addClickHandler(expandBeforeListener);
- Anchor after = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
- after.addClickHandler(expandAfterListener);
- p.add(before);
+ // Only show the expand before/after if skipped more than 30 lines.
+ Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true);
+ Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true);
+
+ b.addClickHandler(expandBeforeListener);
+ a.addClickHandler(expandAfterListener);
+
+ b.setStyleName(Gerrit.RESOURCES.css().skipLine());
+ a.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
+ p.add(b);
p.add(l1);
p.add(all);
p.add(l2);
- p.add(after);
+ p.add(a);
} else {
p.add(l1);
p.add(all);
p.add(l2);
}
- setWidget(row, 1, p);
+ table.setWidget(row, 1, p);
}
private void openLine(final SafeHtmlBuilder m) {
@@ -447,22 +392,34 @@ public class SideBySideTable extends AbstractPatchContentTable {
m.closeTd();
}
- private void appendLineText(final SafeHtmlBuilder m,
- final int lineNumberMinusOne, final PatchLine.Type type,
- final SparseHtmlFile src, final int i, final boolean fullBlock) {
- appendLineText(m, lineNumberMinusOne, type, //
- src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+ private void appendLineNumber(SafeHtmlBuilder m, boolean right) {
+ m.openTd();
+ m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+ if (right) {
+ m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+ }
+ m.closeTd();
}
- private void appendLineText(final SafeHtmlBuilder m,
- final int lineNumberMinusOne, final PatchLine.Type type,
- final SafeHtml lineHtml, final boolean trailingEdit,
- final boolean fullBlock) {
+ private void appendLineNumber(SafeHtmlBuilder m, int lineNumberMinusOne, boolean right) {
m.openTd();
m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (lineNumberMinusOne + 1) + "</a>"));
+ if (right) {
+ m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+ }
+ m.append(SafeHtml.asis("<a href=\"javascript:;\">"+ (lineNumberMinusOne + 1) + "</a>"));
m.closeTd();
+ }
+
+ private void appendLineText(final SafeHtmlBuilder m,
+ final PatchLine.Type type, final SparseHtmlFile src, final int i,
+ final boolean fullBlock) {
+ appendLineText(m, type, src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+ }
+ private void appendLineText(final SafeHtmlBuilder m,
+ final PatchLine.Type type, final SafeHtml lineHtml,
+ final boolean trailingEdit, final boolean fullBlock) {
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().fileLine());
switch (type) {
@@ -488,10 +445,6 @@ public class SideBySideTable extends AbstractPatchContentTable {
private void appendLineNone(final SafeHtmlBuilder m, final PatchLine.Type type) {
m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.closeTd();
-
- m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().fileLine());
switch (type != null ? type : PatchLine.Type.CONTEXT) {
case DELETE:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
new file mode 100644
index 0000000000..ace4a49c1e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
@@ -0,0 +1,28 @@
+// 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.client.plugins;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class PluginInfo extends JavaScriptObject {
+
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String version() /*-{ return this.version; }-*/;
+ public final native boolean isDisabled()
+ /*-{ return this.disabled ? true : false; }-*/;
+
+ protected PluginInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
new file mode 100644
index 0000000000..6eca2065dd
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
@@ -0,0 +1,30 @@
+// 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.client.plugins;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Plugins available from {@code /plugins/}. */
+public class PluginMap extends NativeMap<PluginInfo> {
+ public static void all(AsyncCallback<PluginMap> callback) {
+ new RestApi("/plugins/").addParameterTrue("all")
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ protected PluginMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
new file mode 100644
index 0000000000..80c1febbd7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -0,0 +1,46 @@
+// 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.client.projects;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+public class ProjectInfo
+ extends JavaScriptObject
+ implements SuggestOracle.Suggestion {
+ public final Project.NameKey name_key() {
+ return new Project.NameKey(name());
+ }
+
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String description() /*-{ return this.description; }-*/;
+
+ @Override
+ public final String getDisplayString() {
+ if (description() != null) {
+ return name() + " (" + description() + ")";
+ }
+ return name();
+ }
+
+ @Override
+ public final String getReplacementString() {
+ return name();
+ }
+
+ protected ProjectInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
new file mode 100644
index 0000000000..408919edf3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -0,0 +1,58 @@
+// 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.client.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.http.client.URL;
+
+/** Projects available from {@code /projects/}. */
+public class ProjectMap extends NativeMap<ProjectInfo> {
+ public static void all(AsyncCallback<ProjectMap> callback) {
+ new RestApi("/projects/")
+ .addParameterRaw("type", "ALL")
+ .addParameterTrue("all")
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void permissions(AsyncCallback<ProjectMap> callback) {
+ new RestApi("/projects/")
+ .addParameterRaw("type", "PERMISSIONS")
+ .addParameterTrue("all")
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void parentCandidates(AsyncCallback<ProjectMap> callback) {
+ new RestApi("/projects/")
+ .addParameterRaw("type", "PARENT_CANDIDATES")
+ .addParameterTrue("all")
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
+ new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
+ .addParameterRaw("type", "ALL")
+ .addParameter("n", limit)
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(cb));
+ }
+
+ protected ProjectMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index 98ae46f514..dce5bb6b5a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -37,8 +37,11 @@ public abstract class GerritCallback<T> implements AsyncCallback<T> {
new NotSignedInDialog().center();
} else if (isNoSuchEntity(caught)) {
- new ErrorDialog(Gerrit.C.notFoundBody()).center();
-
+ if (Gerrit.isSignedIn()) {
+ new ErrorDialog(Gerrit.C.notFoundBody()).center();
+ } else {
+ new NotSignedInDialog().center();
+ }
} else if (isInactiveAccount(caught)) {
new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
new file mode 100644
index 0000000000..e820fe060f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
@@ -0,0 +1,55 @@
+// 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.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/** A read-only list of native JavaScript objects stored in a JSON array. */
+public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
+ protected NativeList() {
+ }
+
+ public final List<T> asList() {
+ return new AbstractList<T>() {
+ @Override
+ public T set(int index, T element) {
+ T old = NativeList.this.get(index);
+ NativeList.this.set0(index, element);
+ return old;
+ }
+
+ @Override
+ public T get(int index) {
+ return NativeList.this.get(index);
+ }
+
+ @Override
+ public int size() {
+ return NativeList.this.size();
+ }
+ };
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public final native int size() /*-{ return this.length; }-*/;
+ public final native T get(int i) /*-{ return this[i]; }-*/;
+ private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
new file mode 100644
index 0000000000..cde9041bc0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -0,0 +1,93 @@
+// 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.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+import java.util.Set;
+
+/** A map of native JSON objects, keyed by a string. */
+public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+ /**
+ * Loop through the result map's entries and copy the key strings into the
+ * "name" property of the corresponding child object. This only runs on the
+ * top level map of the result, and requires the children to be JSON objects
+ * and not a JSON primitive (e.g. boolean or string).
+ */
+ public static <T extends JavaScriptObject,
+ M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+ AsyncCallback<M> callback) {
+ return copyKeysIntoChildren("name", callback);
+ }
+
+ /** Loop through the result map and set asProperty on the children. */
+ public static <T extends JavaScriptObject,
+ M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+ final String asProperty, AsyncCallback<M> callback) {
+ return new TransformCallback<M, M>(callback) {
+ @Override
+ protected M transform(M result) {
+ result.copyKeysIntoChildren(asProperty);
+ return result;
+ }
+ };
+ }
+
+ protected NativeMap() {
+ }
+
+ public final Set<String> keySet() {
+ return Natives.keys(this);
+ }
+
+ public final native NativeList<T> values()
+ /*-{
+ var s = this;
+ var v = [];
+ var i = 0;
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ v[i++] = s[k];
+ }
+ }
+ return v;
+ }-*/;
+
+ public final int size() {
+ return keySet().size();
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public final boolean containsKey(String n) {
+ return get(n) != null;
+ }
+
+ public final native T get(String n) /*-{ return this[n]; }-*/;
+
+ public final native void copyKeysIntoChildren(String p)
+ /*-{
+ var s = this;
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ var c = s[k];
+ c[p] = k;
+ }
+ }
+ }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
new file mode 100644
index 0000000000..a6c609c83c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -0,0 +1,57 @@
+// 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.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class Natives {
+ /**
+ * Get the names of defined properties on the object. The returned set
+ * iterates in the native iteration order, which may match the source order.
+ */
+ public static Set<String> keys(JavaScriptObject obj) {
+ if (obj != null) {
+ return new JSONObject(obj).keySet();
+ }
+ return Collections.emptySet();
+ }
+
+ public static <T extends JavaScriptObject> T parseJSON(String json) {
+ if (parser == null) {
+ parser = bestJsonParser();
+ }
+ // javac generics bug
+ return Natives.<T>parse0(parser, json);
+ }
+
+ private static native <T extends JavaScriptObject>
+ T parse0(JavaScriptObject p, String s)
+ /*-{ return p(s); }-*/;
+
+ private static JavaScriptObject parser;
+ private static native JavaScriptObject bestJsonParser()
+ /*-{
+ if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
+ return $wnd.JSON.parse;
+ return function(s) { return eval('(' + s + ')'); };
+ }-*/;
+
+ private Natives() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
new file mode 100644
index 0000000000..650cacdabb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -0,0 +1,215 @@
+// 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.client.rpc;
+
+import com.google.gerrit.client.RpcStatus;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwtjsonrpc.client.RemoteJsonException;
+import com.google.gwtjsonrpc.client.ServerUnavailableException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.JsonConstants;
+
+/** Makes a REST API call to the server. */
+public class RestApi {
+ /**
+ * Expected JSON content body prefix that prevents XSSI.
+ * <p>
+ * The server always includes this line as the first line of the response
+ * content body when the response body is formatted as JSON. It gets inserted
+ * by the server to prevent the resource from being imported into another
+ * domain's page using a &lt;script&gt; tag. This line must be removed before
+ * the JSON can be parsed.
+ */
+ private static final String JSON_MAGIC = ")]}'\n";
+
+ private class MyRequestCallback<T extends JavaScriptObject> implements
+ RequestCallback {
+ private final boolean wasGet;
+ private final AsyncCallback<T> cb;
+
+ public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) {
+ this.wasGet = wasGet;
+ this.cb = cb;
+ }
+
+ @Override
+ public void onResponseReceived(Request req, Response res) {
+ int status = res.getStatusCode();
+ if (status != 200) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ if ((400 <= status && status < 600) && isTextBody(res)) {
+ cb.onFailure(new RemoteJsonException(res.getText(), status, null));
+ } else {
+ cb.onFailure(new StatusCodeException(status, res.getStatusText()));
+ }
+ return;
+ }
+
+ if (!isJsonBody(res)) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+
+ String json = res.getText();
+ if (!json.startsWith(JSON_MAGIC)) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+ json = json.substring(JSON_MAGIC.length());
+
+ if (wasGet && json.startsWith("{\"_authkey\":")) {
+ RestApi.this.resendPost(cb, json);
+ return;
+ }
+
+ T data;
+ try {
+ // javac generics bug
+ data = Natives.<T> parseJSON(json);
+ } catch (RuntimeException e) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+
+ cb.onSuccess(data);
+ RpcStatus.INSTANCE.onRpcComplete();
+ }
+
+ @Override
+ public void onError(Request req, Throwable err) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ if (err.getMessage().contains("XmlHttpRequest.status")) {
+ cb.onFailure(new ServerUnavailableException());
+ } else {
+ cb.onFailure(err);
+ }
+ }
+ }
+
+ private StringBuilder url;
+ private boolean hasQueryParams;
+
+ /**
+ * Initialize a new API call.
+ * <p>
+ * By default the JSON format will be selected by including an HTTP Accept
+ * header in the request.
+ *
+ * @param name URL of the REST resource to access, e.g. {@code "/projects/"}
+ * to list accessible projects from the server.
+ */
+ public RestApi(String name) {
+ if (name.startsWith("/")) {
+ name = name.substring(1);
+ }
+
+ url = new StringBuilder();
+ url.append(GWT.getHostPageBaseURL());
+ url.append(name);
+ }
+
+ public RestApi addParameter(String name, String value) {
+ return addParameterRaw(name, URL.encodeQueryString(value));
+ }
+
+ public RestApi addParameterTrue(String name) {
+ return addParameterRaw(name, null);
+ }
+
+ public RestApi addParameter(String name, boolean value) {
+ return addParameterRaw(name, value ? "t" : "f");
+ }
+
+ public RestApi addParameter(String name, int value) {
+ return addParameterRaw(name, String.valueOf(value));
+ }
+
+ public RestApi addParameter(String name, Enum<?> value) {
+ return addParameterRaw(name, value.name());
+ }
+
+ public RestApi addParameterRaw(String name, String value) {
+ if (hasQueryParams) {
+ url.append("&");
+ } else {
+ url.append("?");
+ hasQueryParams = true;
+ }
+ url.append(name);
+ if (value != null) {
+ url.append("=").append(value);
+ }
+ return this;
+ }
+
+ public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
+ RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
+ req.setHeader("Accept", JsonConstants.JSON_TYPE);
+ req.setCallback(new MyRequestCallback<T>(true, cb));
+ try {
+ RpcStatus.INSTANCE.onRpcStart();
+ req.send();
+ } catch (RequestException e) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(e);
+ }
+ }
+
+ private <T extends JavaScriptObject> void resendPost(
+ final AsyncCallback<T> cb, String token) {
+ RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString());
+ req.setHeader("Accept", JsonConstants.JSON_TYPE);
+ req.setHeader("Content-Type", JsonConstants.JSON_TYPE);
+ req.setRequestData(token);
+ req.setCallback(new MyRequestCallback<T>(false, cb));
+ try {
+ req.send();
+ } catch (RequestException e) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(e);
+ }
+ }
+
+ private static boolean isJsonBody(Response res) {
+ return isContentType(res, JsonConstants.JSON_TYPE);
+ }
+
+ private static boolean isTextBody(Response res) {
+ return isContentType(res, "text/plain");
+ }
+
+ private static boolean isContentType(Response res, String want) {
+ String type = res.getHeader("Content-Type");
+ if (type == null) {
+ return false;
+ }
+ int semi = type.indexOf(';');
+ if (semi >= 0) {
+ type = type.substring(0, semi).trim();
+ }
+ return want.equals(type);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
new file mode 100644
index 0000000000..2cd22cb4e3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -0,0 +1,38 @@
+// 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.client.rpc;
+
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Transforms a value and passes it on to another callback. */
+public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+ private final AsyncCallback<O> callback;
+
+ protected TransformCallback(AsyncCallback<O> callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onSuccess(I result) {
+ callback.onSuccess(transform(result));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ callback.onFailure(caught);
+ }
+
+ protected abstract O transform(I result);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 885f53b0d9..5da00cd1c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -18,6 +18,7 @@ import com.google.gerrit.client.RpcStatus;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
@@ -31,11 +32,14 @@ public class AccountGroupSuggestOracle extends HighlightSuggestOracle {
private Map<String, AccountGroup.UUID> priorResults =
new HashMap<String, AccountGroup.UUID>();
+ private Project.NameKey projectName;
+
@Override
public void onRequestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() {
public void run() {
- SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
+ SuggestUtil.SVC.suggestAccountGroupForProject(
+ projectName, req.getQuery(), req.getLimit(),
new GerritCallback<List<GroupReference>>() {
public void onSuccess(final List<GroupReference> result) {
priorResults.clear();
@@ -52,6 +56,10 @@ public class AccountGroupSuggestOracle extends HighlightSuggestOracle {
});
}
+ public void setProject(Project.NameKey projectName) {
+ this.projectName = projectName;
+ }
+
private static class AccountGroupSuggestion implements
SuggestOracle.Suggestion {
private final GroupReference info;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
index 5233a6b86d..790102c125 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// 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.
@@ -16,41 +16,36 @@ package com.google.gerrit.client.ui;
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.reviewdb.client.Account;
/** Link to any user's account dashboard. */
-public class AccountDashboardLink extends InlineHyperlink {
+public class AccountLink extends InlineHyperlink {
/** Create a link after locating account details from an active cache. */
- public static AccountDashboardLink link(final AccountInfoCache cache,
+ public static AccountLink link(final AccountInfoCache cache,
final Account.Id id) {
final AccountInfo ai = cache.get(id);
- return ai != null ? new AccountDashboardLink(ai) : null;
+ return ai != null ? new AccountLink(ai) : null;
}
- private Account.Id accountId;
-
- public AccountDashboardLink(final AccountInfo ai) {
- this(FormatUtil.name(ai), ai);
- }
-
- public AccountDashboardLink(final String text, final AccountInfo ai) {
- this(text, ai.getId());
+ public AccountLink(final AccountInfo ai) {
+ super(FormatUtil.name(ai), PageLinks.toAccountQuery(owner(ai)));
setTitle(FormatUtil.nameEmail(ai));
}
- public AccountDashboardLink(final String text, final Account.Id ai) {
- super(text, PageLinks.toAccountDashboard(ai));
- addStyleName(Gerrit.RESOURCES.css().accountName());
- accountId = ai;
+ private static String owner(AccountInfo ai) {
+ if (ai.getPreferredEmail() != null) {
+ return ai.getPreferredEmail();
+ } else if (ai.getFullName() != null) {
+ return ai.getFullName();
+ }
+ return "" + ai.getId().get();
}
@Override
public void go() {
- Gerrit.display(getTargetHistoryToken(), //
- new AccountDashboardScreen(accountId));
+ Gerrit.display(getTargetHistoryToken());
}
}
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 0cea2c71f5..ddd2b27fb6 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
@@ -61,7 +61,7 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers,
setMessageText(message);
setAuthorNameText(FormatUtil.name(author));
- setDateText(FormatUtil.shortFormat(when));
+ setDateText(FormatUtil.shortFormatDayTime(when));
final CellFormatter fmt = header.getCellFormatter();
fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
new file mode 100644
index 0000000000..217ca5aace
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2011 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.ui;
+
+import com.google.gerrit.client.account.Util;
+import com.google.gerrit.client.projects.ProjectMap;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
+import com.google.gwtexpui.user.client.PluginSafeDialogBox;
+
+/** It creates a popup containing all the projects. */
+public class ProjectListPopup {
+ private ProjectsTable projectsTab;
+ private PluginSafeDialogBox popup;
+ private Button close;
+ private ScrollPanel sp;
+ private PopupPanel.PositionCallback popupPosition;
+ private int preferredTop;
+ private int preferredLeft;
+ private boolean popingUp;
+ private boolean firstPopupLoad = true;
+
+ public void initPopup(final String popupText, final String currentPageLink) {
+ createWidgets(popupText, currentPageLink);
+ final FlowPanel pfp = new FlowPanel();
+ sp = new ScrollPanel(projectsTab);
+ sp.setSize("100%", "100%");
+ pfp.add(sp);
+ pfp.add(close);
+ popup.setWidget(pfp);
+ popup.setHeight("100%");
+ popupPosition = getPositionCallback();
+ }
+
+ protected PopupPanel.PositionCallback getPositionCallback() {
+ return new PopupPanel.PositionCallback() {
+ @Override
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ if (preferredTop + offsetHeight > Window.getClientWidth()) {
+ preferredTop = Window.getClientWidth() - offsetHeight;
+ }
+ if (preferredLeft + offsetWidth > Window.getClientWidth()) {
+ preferredLeft = Window.getClientWidth() - offsetWidth;
+ }
+
+ if (preferredTop < 0) {
+ sp.setHeight((sp.getOffsetHeight() + preferredTop) + "px");
+ preferredTop = 0;
+ }
+ if (preferredLeft < 0) {
+ sp.setWidth((sp.getOffsetWidth() + preferredLeft) + "px");
+ preferredLeft = 0;
+ }
+
+ popup.setPopupPosition(preferredLeft, preferredTop);
+ }
+ };
+ }
+
+ protected void onMovePointerTo(String projectName) {
+ }
+
+ protected void openRow(String projectName) {
+ }
+
+ public boolean isPopingUp() {
+ return popingUp;
+ }
+
+ private void createWidgets(final String popupText,
+ final String currentPageLink) {
+ projectsTab = new ProjectsTable() {
+ @Override
+ protected void movePointerTo(final int row, final boolean scroll) {
+ super.movePointerTo(row, scroll);
+ onMovePointerTo(getRowItem(row).name());
+ }
+
+ @Override
+ protected void onOpenRow(final int row) {
+ super.onOpenRow(row);
+ openRow(getRowItem(row).name());
+ }
+ };
+ projectsTab.setSavePointerId(currentPageLink);
+
+ close = new Button(Util.C.projectsClose());
+ close.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ closePopup();
+ }
+ });
+
+ popup = new PluginSafeDialogBox();
+ popup.setModal(false);
+ popup.setText(popupText);
+ }
+
+ public void displayPopup() {
+ popingUp = true;
+ if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
+ populateProjects();
+ } else {
+ popup.setPopupPositionAndShow(popupPosition);
+ GlobalKey.dialog(popup);
+ try {
+ GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
+ KeyCodes.KEY_ESCAPE, popup));
+ } catch (Throwable e) {
+ }
+ projectsTab.setRegisterKeys(true);
+ projectsTab.finishDisplay();
+ popingUp = false;
+ }
+ }
+
+ public void closePopup() {
+ popup.hide();
+ }
+
+ public void setPreferredCoordinates(final int top, final int left) {
+ this.preferredTop = top;
+ this.preferredLeft = left;
+ }
+
+ protected void populateProjects() {
+ ProjectMap.all(new GerritCallback<ProjectMap>() {
+ @Override
+ public void onSuccess(final ProjectMap result) {
+ projectsTab.display(result);
+ if (firstPopupLoad) { // Display was delayed until table was loaded
+ firstPopupLoad = false;
+ displayPopup();
+ }
+ }
+ });
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index be82effe80..25ed25810c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -15,49 +15,25 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
/** Suggestion Oracle for Project.NameKey entities. */
public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
@Override
public void onRequestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() {
+ @Override
public void run() {
- SuggestUtil.SVC.suggestProjectNameKey(req.getQuery(), req.getLimit(),
- new GerritCallback<List<Project.NameKey>>() {
- public void onSuccess(final List<Project.NameKey> result) {
- final ArrayList<ProjectNameSuggestion> r =
- new ArrayList<ProjectNameSuggestion>(result.size());
- for (final Project.NameKey p : result) {
- r.add(new ProjectNameSuggestion(p));
- }
- callback.onSuggestionsReady(req, new Response(r));
+ ProjectMap.suggest(req.getQuery(), req.getLimit(),
+ new GerritCallback<ProjectMap>() {
+ @Override
+ public void onSuccess(ProjectMap map) {
+ callback.onSuggestionsReady(req, new Response(map.values().asList()));
}
});
}
});
}
-
- private static class ProjectNameSuggestion implements
- SuggestOracle.Suggestion {
- private final Project.NameKey key;
-
- ProjectNameSuggestion(final Project.NameKey k) {
- key = k;
- }
-
- public String getDisplayString() {
- return key.get();
- }
-
- public String getReplacementString() {
- return key.get();
- }
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index b7686431bd..0cbe19415e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -15,16 +15,19 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
-public class ProjectsTable extends NavigationTable<Project> {
+public class ProjectsTable extends NavigationTable<ProjectInfo> {
public ProjectsTable() {
keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev()));
@@ -32,7 +35,10 @@ public class ProjectsTable extends NavigationTable<Project> {
keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
Util.C.projectListOpen()));
+ initColumnHeaders();
+ }
+ protected void initColumnHeaders() {
table.setText(0, 1, Util.C.projectName());
table.setText(0, 2, Util.C.projectDescription());
@@ -41,6 +47,7 @@ public class ProjectsTable extends NavigationTable<Project> {
fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
}
+ @Override
protected MyFlexTable createFlexTable() {
MyFlexTable table = new MyFlexTable() {
@Override
@@ -78,8 +85,8 @@ public class ProjectsTable extends NavigationTable<Project> {
}
@Override
- protected Object getRowItemKey(final Project item) {
- return item.getNameKey();
+ protected Object getRowItemKey(final ProjectInfo item) {
+ return item.name();
}
@Override
@@ -89,17 +96,24 @@ public class ProjectsTable extends NavigationTable<Project> {
}
}
- public void display(final List<Project> projects) {
+ public void display(ProjectMap projects) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final Project k : projects)
- insert(table.getRowCount(), k);
+ List<ProjectInfo> list = projects.values().asList();
+ Collections.sort(list, new Comparator<ProjectInfo>() {
+ @Override
+ public int compare(ProjectInfo a, ProjectInfo b) {
+ return a.name().compareTo(b.name());
+ }
+ });
+ for(ProjectInfo p : list)
+ insert(table.getRowCount(), p);
finishDisplay();
}
- protected void insert(final int row, final Project k) {
+ protected void insert(final int row, final ProjectInfo k) {
table.insertRow(row);
applyDataRowStyle(row);
@@ -112,9 +126,9 @@ public class ProjectsTable extends NavigationTable<Project> {
populate(row, k);
}
- protected void populate(final int row, final Project k) {
- table.setText(row, 1, k.getName());
- table.setText(row, 2, k.getDescription());
+ protected void populate(final int row, final ProjectInfo k) {
+ table.setText(row, 1, k.name());
+ table.setText(row, 2, k.description());
setRowItem(row, k);
}
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 5cb47271b5..845a046556 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,8 +15,6 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -97,6 +95,10 @@ public abstract class Screen extends View {
}
}
+ protected void setHeaderVisible(boolean value) {
+ header.setVisible(value);
+ }
+
protected void setTitleEast(final Widget w) {
header.setWidget(0, Cols.East.ordinal(), w);
}
@@ -148,13 +150,6 @@ public abstract class Screen extends View {
return requiresSignIn;
}
- /** Invoked if this screen is the current screen and the user signs out. */
- public void onSignOut() {
- if (isRequiresSignIn()) {
- History.newItem(PageLinks.toChangeQuery("status:open"));
- }
- }
-
public void onShowView() {
if (windowTitle != null) {
Gerrit.setWindowTitle(this, windowTitle);
diff --git a/gerrit-httpd/.gitignore b/gerrit-httpd/.gitignore
index 194bedcbc4..5bbeafde75 100644
--- a/gerrit-httpd/.gitignore
+++ b/gerrit-httpd/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-httpd.iml \ No newline at end of file
diff --git a/gerrit-httpd/.settings/org.eclipse.core.resources.prefs b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
index 9df523e4f6..839d647eef 100644
--- a/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index a6374da024..ceacb669e5 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
new file mode 100644
index 0000000000..c8d237c563
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -0,0 +1,86 @@
+// 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.httpd;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/** Filters all HTTP requests passing through the server. */
+public abstract class AllRequestFilter implements Filter {
+ public static ServletModule module() {
+ return new ServletModule() {
+ @Override
+ protected void configureServlets() {
+ DynamicSet.setOf(binder(), AllRequestFilter.class);
+ filter("/*").through(FilterProxy.class);
+ }
+ };
+ }
+
+ @Singleton
+ static class FilterProxy implements Filter {
+ private final DynamicSet<AllRequestFilter> filters;
+
+ @Inject
+ FilterProxy(DynamicSet<AllRequestFilter> filters) {
+ this.filters = filters;
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res,
+ final FilterChain last) throws IOException, ServletException {
+ final Iterator<AllRequestFilter> itr = filters.iterator();
+ new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res)
+ throws IOException, ServletException {
+ if (itr.hasNext()) {
+ itr.next().doFilter(req, res, this);
+ } else {
+ last.doFilter(req, res);
+ }
+ }
+ }.doFilter(req, res);
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index fb30a4db0a..ca3d287105 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -25,17 +25,17 @@ import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
import com.google.inject.servlet.RequestScoped;
+import org.eclipse.jgit.http.server.GitSmartHttpTools;
+
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -49,13 +49,9 @@ public final class CacheBasedWebSession implements WebSession {
return new CacheModule() {
@Override
protected void configure() {
- final String cacheName = WebSessionManager.CACHE_NAME;
- final TypeLiteral<Cache<Key, Val>> type =
- new TypeLiteral<Cache<Key, Val>>() {};
- disk(type, cacheName) //
- .memoryLimit(1024) // reasonable default for many sites
- .maxAge(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
- .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
+ persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
+ .maximumWeight(1024) // reasonable default for many sites
+ .expireAfterWrite(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
;
bind(WebSessionManager.class);
bind(WebSession.class)
@@ -71,8 +67,9 @@ public final class CacheBasedWebSession implements WebSession {
private final AuthConfig authConfig;
private final Provider<AnonymousUser> anonymousProvider;
private final IdentifiedUser.RequestFactory identified;
- private AccessPath accessPath = AccessPath.WEB_UI;
+ private AccessPath accessPath;
private Cookie outCookie;
+ private AuthMethod authMethod;
private Key key;
private Val val;
@@ -90,6 +87,12 @@ public final class CacheBasedWebSession implements WebSession {
this.anonymousProvider = anonymousProvider;
this.identified = identified;
+ if (GitSmartHttpTools.isGitClient(request)) {
+ accessPath = AccessPath.GIT;
+ } else {
+ accessPath = AccessPath.WEB_UI;
+ }
+
final String cookie = readCookie();
if (cookie != null) {
key = new Key(cookie);
@@ -98,6 +101,7 @@ public final class CacheBasedWebSession implements WebSession {
key = null;
val = null;
}
+ authMethod = isSignedIn() ? AuthMethod.COOKIE : AuthMethod.NONE;
if (isSignedIn() && val.needsCookieRefresh()) {
// Cookie is more than half old. Send the cookie again to the
@@ -149,7 +153,8 @@ public final class CacheBasedWebSession implements WebSession {
return anonymousProvider.get();
}
- public void login(final AuthResult res, final boolean rememberMe) {
+ public void login(final AuthResult res, final AuthMethod meth,
+ final boolean rememberMe) {
final Account.Id id = res.getAccountId();
final AccountExternalId.Key identity = res.getExternalId();
@@ -160,17 +165,15 @@ public final class CacheBasedWebSession implements WebSession {
key = manager.createKey(id);
val = manager.createVal(key, id, rememberMe, identity, null);
saveCookie();
- }
- /** Change the access path from the default of {@link AccessPath#WEB_UI}. */
- public void setAccessPath(AccessPath path) {
- accessPath = path;
+ authMethod = meth;
}
/** Set the user account for this current request only. */
- public void setUserAccountId(Account.Id id) {
+ public void setUserAccountId(Account.Id id, AuthMethod method) {
key = new Key("id:" + id);
- val = new Val(id, 0, false, null, "");
+ val = new Val(id, 0, false, null, "", 0);
+ authMethod = method;
}
public void logout() {
@@ -217,4 +220,8 @@ public final class CacheBasedWebSession implements WebSession {
private static boolean isSecure(final HttpServletRequest req) {
return req.isSecure() || "https".equals(req.getScheme());
}
+
+ public AuthMethod getAuthMethod() {
+ return authMethod;
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index c0e3f424ab..9ce2298518 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -19,13 +19,12 @@ import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtjsonrpc.server.XsrfException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
@@ -39,7 +38,6 @@ import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
/**
* Trust the authentication which is done by the container.
@@ -62,7 +60,7 @@ class ContainerAuthFilter implements Filter {
@Inject
ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
- @GerritServerConfig Config config) throws XsrfException {
+ @GerritServerConfig Config config) {
this.session = session;
this.accountCache = accountCache;
this.config = config;
@@ -80,20 +78,14 @@ class ContainerAuthFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
- if (!GitSmartHttpTools.isGitClient(req)) {
- chain.doFilter(request, response);
- return;
- }
-
- HttpServletResponseWrapper rsp =
- new HttpServletResponseWrapper((HttpServletResponse) response);
+ HttpServletResponse rsp = (HttpServletResponse) response;
if (verify(req, rsp)) {
chain.doFilter(req, response);
}
}
- private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
+ private boolean verify(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
String username = req.getRemoteUser();
if (username == null) {
@@ -108,7 +100,9 @@ class ContainerAuthFilter implements Filter {
rsp.sendError(SC_UNAUTHORIZED);
return false;
}
- session.get().setUserAccountId(who.getAccount().getId());
+ session.get().setUserAccountId(
+ who.getAccount().getId(),
+ AuthMethod.PASSWORD);
return true;
}
}
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 1953480cce..c1f3ae4293 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
@@ -22,7 +22,7 @@ import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.DownloadSchemeConfig;
+import com.google.gerrit.server.config.DownloadConfig;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailSender;
@@ -48,7 +48,7 @@ class GerritConfigProvider implements Provider<GerritConfig> {
private final Realm realm;
private final Config cfg;
private final AuthConfig authConfig;
- private final DownloadSchemeConfig schemeConfig;
+ private final DownloadConfig downloadConfig;
private final GitWebConfig gitWebConfig;
private final AllProjectsName wildProject;
private final SshInfo sshInfo;
@@ -63,12 +63,12 @@ class GerritConfigProvider implements Provider<GerritConfig> {
GerritConfigProvider(final Realm r, @GerritServerConfig final Config gsc,
final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
final SshInfo si, final ApprovalTypes at, final ContactStore cs,
- final ServletContext sc, final DownloadSchemeConfig dc,
+ final ServletContext sc, final DownloadConfig dc,
final @AnonymousCowardName String acn) {
realm = r;
cfg = gsc;
authConfig = ac;
- schemeConfig = dc;
+ downloadConfig = dc;
gitWebConfig = gwc;
sshInfo = si;
wildProject = wp;
@@ -90,13 +90,19 @@ class GerritConfigProvider implements Provider<GerritConfig> {
config.setAllowedOpenIDs(authConfig.getAllowedOpenIDs());
break;
+ case OPENID_SSO:
+ config.setOpenIdSsoUrl(authConfig.getOpenIdSsoUrl());
+ break;
+
case LDAP:
case LDAP_BIND:
config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+ config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
break;
case CUSTOM_EXTENSION:
config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+ config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
break;
}
@@ -105,7 +111,8 @@ class GerritConfigProvider implements Provider<GerritConfig> {
config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
config.setGitHttpUrl(cfg.getString("gerrit", null, "gitHttpUrl"));
config.setUseContactInfo(contactStore != null && contactStore.isEnabled());
- config.setDownloadSchemes(schemeConfig.getDownloadScheme());
+ config.setDownloadSchemes(downloadConfig.getDownloadSchemes());
+ config.setDownloadCommands(downloadConfig.getDownloadCommands());
config.setAuthType(authConfig.getAuthType());
config.setWildProject(wildProject);
config.setApprovalTypes(approvalTypes);
@@ -115,6 +122,13 @@ class GerritConfigProvider implements Provider<GerritConfig> {
"test", false));
config.setAnonymousCowardName(anonymousCowardName);
+ config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
+ if (config.getReportBugUrl() == null) {
+ config.setReportBugUrl("http://code.google.com/p/gerrit/issues/list");
+ } else if (config.getReportBugUrl().isEmpty()) {
+ config.setReportBugUrl(null);
+ }
+
final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
for (final Account.FieldName n : Account.FieldName.values()) {
if (realm.allowsEdit(n)) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 6fd94c94cb..6ca9949295 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -34,6 +34,8 @@ public class GitOverHttpModule extends ServletModule {
Class<? extends Filter> authFilter;
if (authConfig.isTrustContainerAuth()) {
authFilter = ContainerAuthFilter.class;
+ } else if (authConfig.isGitBasichAuth()) {
+ authFilter = ProjectBasicAuthFilter.class;
} else {
authFilter = ProjectDigestFilter.class;
}
@@ -41,5 +43,7 @@ public class GitOverHttpModule extends ServletModule {
String git = GitOverHttpServlet.URL_REGEX;
filterRegex(git).through(authFilter);
serveRegex(git).with(GitOverHttpServlet.class);
+
+ filter("/a/*").through(authFilter);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index c36df04a68..6bd35ddff4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,13 +14,12 @@
package com.google.gerrit.httpd;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -44,7 +43,6 @@ import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.http.server.resolver.AsIsFileService;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.pack.PackConfig;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.UploadPack;
import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
@@ -99,11 +97,11 @@ public class GitOverHttpServlet extends GitServlet {
install(new CacheModule() {
@Override
protected void configure() {
- TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>> cache =
- new TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>>() {};
- core(cache, ID_CACHE)
- .memoryLimit(4096)
- .maxAge(10, TimeUnit.MINUTES);
+ cache(ID_CACHE,
+ AdvertisedObjectsCacheKey.class,
+ new TypeLiteral<Set<ObjectId>>() {})
+ .maximumWeight(4096)
+ .expireAfterWrite(10, TimeUnit.MINUTES);
}
});
}
@@ -167,25 +165,30 @@ public class GitOverHttpServlet extends GitServlet {
}
req.setAttribute(ATT_CONTROL, pc);
- return manager.openRepository(pc.getProject().getNameKey());
+ try {
+ return manager.openRepository(pc.getProject().getNameKey());
+ } catch (IOException e) {
+ throw new RepositoryNotFoundException(
+ pc.getProject().getNameKey().get(), e);
+ }
}
}
static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
- private final PackConfig packConfig;
+ private final TransferConfig config;
private final Provider<WebSession> session;
@Inject
UploadFactory(TransferConfig tc, Provider<WebSession> session) {
- this.packConfig = tc.getPackConfig();
+ this.config = tc;
this.session = session;
}
@Override
public UploadPack create(HttpServletRequest req, Repository repo) {
UploadPack up = new UploadPack(repo);
- up.setPackConfig(packConfig);
- session.get().setAccessPath(AccessPath.GIT);
+ up.setPackConfig(config.getPackConfig());
+ up.setTimeout(config.getTimeout());
return up;
}
}
@@ -234,12 +237,14 @@ public class GitOverHttpServlet extends GitServlet {
static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
private final AsyncReceiveCommits.Factory factory;
private final Provider<WebSession> session;
+ private final TransferConfig config;
@Inject
ReceiveFactory(AsyncReceiveCommits.Factory factory,
- Provider<WebSession> session) {
+ Provider<WebSession> session, TransferConfig config) {
this.factory = factory;
this.session = session;
+ this.config = config;
}
@Override
@@ -254,10 +259,12 @@ public class GitOverHttpServlet extends GitServlet {
final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
final ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
- rc.getReceivePack().setRefLogIdent(user.newRefLogIdent());
+ ReceivePack rp = rc.getReceivePack();
+ rp.setRefLogIdent(user.newRefLogIdent());
+ rp.setTimeout(config.getTimeout());
+ rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
req.setAttribute(ATT_RC, rc);
- session.get().setAccessPath(AccessPath.GIT);
- return rc.getReceivePack();
+ return rp;
}
}
@@ -315,12 +322,12 @@ public class GitOverHttpServlet extends GitServlet {
if (isGet) {
rc.advertiseHistory();
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
} else {
- Set<ObjectId> ids = cache.get(cacheKey);
+ Set<ObjectId> ids = cache.getIfPresent(cacheKey);
if (ids != null) {
rp.getAdvertisedObjects().addAll(ids);
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
deleted file mode 100644
index 6c420a5df6..0000000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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.gerrit.httpd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class HttpIdentifiedUserProvider implements Provider<IdentifiedUser> {
- private final Provider<CurrentUser> currUserProvider;
-
- @Inject
- HttpIdentifiedUserProvider(Provider<CurrentUser> currUserProvider) {
- this.currUserProvider = currUserProvider;
- }
-
- @Override
- public IdentifiedUser get() {
- CurrentUser user = currUserProvider.get();
- if (user instanceof IdentifiedUser) {
- return (IdentifiedUser) user;
- }
- throw new ProvisionException(NotSignedInException.MESSAGE,
- new NotSignedInException());
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 13a6f437be..e9b3500a94 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -14,7 +14,10 @@
package com.google.gerrit.httpd;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
import com.google.common.base.Strings;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -36,19 +39,21 @@ class HttpLogoutServlet extends HttpServlet {
private final Provider<WebSession> webSession;
private final Provider<String> urlProvider;
private final String logoutUrl;
+ private final AuditService audit;
@Inject
HttpLogoutServlet(final AuthConfig authConfig,
final Provider<WebSession> webSession,
@CanonicalWebUrl @Nullable final Provider<String> urlProvider,
- final AccountManager accountManager) {
+ final AccountManager accountManager,
+ final AuditService audit) {
this.webSession = webSession;
this.urlProvider = urlProvider;
this.logoutUrl = authConfig.getLogoutURL();
+ this.audit = audit;
}
- @Override
- protected void doGet(final HttpServletRequest req,
+ private void doLogout(final HttpServletRequest req,
final HttpServletResponse rsp) throws IOException {
webSession.get().logout();
if (logoutUrl != null) {
@@ -67,4 +72,22 @@ class HttpLogoutServlet extends HttpServlet {
rsp.sendRedirect(url);
}
}
+
+ @Override
+ protected void doGet(final HttpServletRequest req,
+ final HttpServletResponse rsp) throws IOException {
+
+ final String sid = webSession.get().getToken();
+ final CurrentUser currentUser = webSession.get().getCurrentUser();
+ final String what = "sign out";
+ final long when = System.currentTimeMillis();
+
+ try {
+ doLogout(req, rsp);
+ } finally {
+ audit.dispatch(new AuditEvent(sid, currentUser,
+ what, when, null, null));
+ }
+ }
+
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index c87a14306b..8ef826b1ce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -15,19 +15,19 @@
package com.google.gerrit.httpd;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.RequestContext;
import com.google.inject.Inject;
-import com.google.inject.Provider;
-class HttpCurrentUserProvider implements Provider<CurrentUser> {
+class HttpRequestContext implements RequestContext {
private final WebSession session;
@Inject
- HttpCurrentUserProvider(final WebSession session) {
+ HttpRequestContext(final WebSession session) {
this.session = session;
}
@Override
- public CurrentUser get() {
+ public CurrentUser getCurrentUser() {
return session.getCurrentUser();
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
new file mode 100644
index 0000000000..5b39cb2f9e
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -0,0 +1,202 @@
+// 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.httpd;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Locale;
+
+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;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Authenticates the current user by HTTP basic authentication.
+ * <p>
+ * The current HTTP request is authenticated by looking up the username and
+ * password from the Base64 encoded Authorization header and validating them
+ * against any username/password configured authentication system in Gerrit.
+ * This filter is intended only to protect the {@link ProjectServlet} and its
+ * handled URLs, which provide remote repository access over HTTP.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
+ */
+@Singleton
+class ProjectBasicAuthFilter implements Filter {
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectBasicAuthFilter.class);
+
+ public static final String REALM_NAME = "Gerrit Code Review";
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String LIT_BASIC = "Basic ";
+
+ private final Provider<WebSession> session;
+ private final AccountCache accountCache;
+ private final AccountManager accountManager;
+ private final AuthConfig authConfig;
+
+ @Inject
+ ProjectBasicAuthFilter(Provider<WebSession> session,
+ AccountCache accountCache, AccountManager accountManager,
+ AuthConfig authConfig) {
+ this.session = session;
+ this.accountCache = accountCache;
+ this.accountManager = accountManager;
+ this.authConfig = authConfig;
+ }
+
+ @Override
+ public void init(FilterConfig config) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ Response rsp = new Response((HttpServletResponse) response);
+
+ if (verify(req, rsp)) {
+ chain.doFilter(req, rsp);
+ }
+ }
+
+ private boolean verify(HttpServletRequest req, Response rsp)
+ throws IOException {
+ final String hdr = req.getHeader(AUTHORIZATION);
+ if (hdr == null) {
+ // Allow an anonymous connection through, or it might be using a
+ // session cookie instead of basic authentication.
+ //
+ return true;
+ }
+
+ final byte[] decoded =
+ Base64.decodeBase64(hdr.substring(LIT_BASIC.length()));
+ String usernamePassword = new String(decoded, encoding(req));
+ int splitPos = usernamePassword.indexOf(':');
+ if (splitPos < 1) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ String username = usernamePassword.substring(0, splitPos);
+ String password = usernamePassword.substring(splitPos + 1);
+ if (Strings.isNullOrEmpty(password)) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ if (authConfig.isUserNameToLowerCase()) {
+ username = username.toLowerCase(Locale.US);
+ }
+
+ final AccountState who = accountCache.getByUsername(username);
+ if (who == null || !who.getAccount().isActive()) {
+ log.warn("Authentication failed for " + username
+ + ": account inactive or not provisioned in Gerrit");
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ AuthRequest whoAuth = AuthRequest.forUser(username);
+ whoAuth.setPassword(password);
+
+ try {
+ AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
+ session.get().setUserAccountId(whoAuthResult.getAccountId(),
+ AuthMethod.PASSWORD);
+ return true;
+ } catch (AccountException e) {
+ log.warn("Authentication failed for " + username, e);
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ }
+
+ private String encoding(HttpServletRequest req) {
+ return Objects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
+ }
+
+ class Response extends HttpServletResponseWrapper {
+ private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ Response(HttpServletResponse rsp) {
+ super(rsp);
+ }
+
+ private void status(int sc) {
+ if (sc == SC_UNAUTHORIZED) {
+ StringBuilder v = new StringBuilder();
+ v.append(LIT_BASIC);
+ v.append("realm=\"" + REALM_NAME + "\"");
+ setHeader(WWW_AUTHENTICATE, v.toString());
+ } else if (containsHeader(WWW_AUTHENTICATE)) {
+ setHeader(WWW_AUTHENTICATE, null);
+ }
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ status(sc);
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ status(sc);
+ super.sendError(sc);
+ }
+
+ @Override
+ public void setStatus(int sc, String sm) {
+ status(sc);
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ status(sc);
+ super.setStatus(sc);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index c5b0e90cc7..84aa53297d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -22,6 +22,7 @@ import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtjsonrpc.server.SignedToken;
@@ -30,7 +31,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
@@ -100,12 +100,7 @@ class ProjectDigestFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
- if (!GitSmartHttpTools.isGitClient(req)) {
- chain.doFilter(request, response);
- return;
- }
-
- Response rsp = new Response((HttpServletResponse) response);
+ Response rsp = new Response(req, (HttpServletResponse) response);
if (verify(req, rsp)) {
chain.doFilter(req, rsp);
@@ -170,7 +165,9 @@ class ProjectDigestFilter implements Filter {
if (expect.equals(response)) {
try {
if (tokens.checkToken(nonce, "") != null) {
- session.get().setUserAccountId(who.getAccount().getId());
+ session.get().setUserAccountId(
+ who.getAccount().getId(),
+ AuthMethod.PASSWORD);
return true;
} else {
@@ -281,10 +278,6 @@ class ProjectDigestFilter implements Filter {
return p;
}
- private String getDomain() {
- return urlProvider.get() + "p/";
- }
-
private String newNonce() {
try {
return tokens.newToken("");
@@ -295,11 +288,12 @@ class ProjectDigestFilter implements Filter {
class Response extends HttpServletResponseWrapper {
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
-
+ private final HttpServletRequest req;
Boolean stale;
- Response(HttpServletResponse rsp) {
+ Response(HttpServletRequest req, HttpServletResponse rsp) {
super(rsp);
+ this.req = req;
}
private void status(int sc) {
@@ -307,7 +301,18 @@ class ProjectDigestFilter implements Filter {
StringBuilder v = new StringBuilder();
v.append("Digest");
v.append(" realm=\"" + REALM_NAME + "\"");
- v.append(", domain=\"" + getDomain() + "\"");
+
+ String url = urlProvider.get();
+ if (url == null) {
+ url = req.getContextPath();
+ if (url != null && !url.isEmpty() && !url.endsWith("/")) {
+ url += "/";
+ }
+ }
+ if (url != null && !url.isEmpty()) {
+ v.append(", domain=\"" + url + "\"");
+ }
+
v.append(", qop=\"auth\"");
if (stale != null) {
v.append(", stale=" + stale);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
new file mode 100644
index 0000000000..b46505f0e1
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -0,0 +1,83 @@
+// 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.gerrit.httpd;
+
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/** Executes any pending {@link RequestCleanup} at the end of a request. */
+@Singleton
+public class RequestContextFilter implements Filter {
+ public static Module module() {
+ return new ServletModule() {
+ @Override
+ protected void configureServlets() {
+ filter("/*").through(RequestContextFilter.class);
+ }
+ };
+ }
+
+ private final Provider<RequestCleanup> cleanup;
+ private final Provider<HttpRequestContext> requestContext;
+ private final ThreadLocalRequestContext local;
+
+ @Inject
+ RequestContextFilter(final Provider<RequestCleanup> r,
+ final Provider<HttpRequestContext> c,
+ final ThreadLocalRequestContext l) {
+ cleanup = r;
+ requestContext = c;
+ local = l;
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(final ServletRequest request,
+ final ServletResponse response, final FilterChain chain)
+ throws IOException, ServletException {
+ RequestContext old = local.setContext(requestContext.get());
+ try {
+ try {
+ chain.doFilter(request, response);
+ } finally {
+ cleanup.get().run();
+ }
+ } finally {
+ local.setContext(old);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
index 0e6a567598..499c2a54e5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,7 +14,8 @@
package com.google.gerrit.httpd;
-import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -27,15 +28,16 @@ import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
-/** Executes any pending {@link RequestCleanup} at the end of a request. */
+/** Requires the user to be authenticated over HTTP. */
@Singleton
-class RequestCleanupFilter implements Filter {
- private final Provider<RequestCleanup> cleanup;
+class RequireIdentifiedUserFilter implements Filter {
+ private final Provider<CurrentUser> user;
@Inject
- RequestCleanupFilter(final Provider<RequestCleanup> r) {
- cleanup = r;
+ RequireIdentifiedUserFilter(Provider<CurrentUser> user) {
+ this.user = user;
}
@Override
@@ -47,13 +49,14 @@ class RequestCleanupFilter implements Filter {
}
@Override
- public void doFilter(final ServletRequest request,
- final ServletResponse response, final FilterChain chain)
+ public void doFilter(ServletRequest request,
+ ServletResponse response, FilterChain chain)
throws IOException, ServletException {
- try {
+ if (user.get() instanceof IdentifiedUser) {
chain.doFilter(request, response);
- } finally {
- cleanup.get().run();
+ } else {
+ HttpServletResponse res = (HttpServletResponse) response;
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
new file mode 100644
index 0000000000..99db2f046d
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
@@ -0,0 +1,232 @@
+// 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.httpd;
+
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gwtjsonrpc.common.JsonConstants;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class RestApiServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+ private static final Logger log =
+ LoggerFactory.getLogger(RestApiServlet.class);
+
+ /** MIME type used for a JSON response body. */
+ protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
+
+ /**
+ * Garbage prefix inserted before JSON output to prevent XSSI.
+ * <p>
+ * This prefix is ")]}'\n" and is designed to prevent a web browser from
+ * executing the response body if the resource URI were to be referenced using
+ * a &lt;script src="...&gt; HTML tag from another web site. Clients using the
+ * HTTP interface will need to always strip the first line of response data to
+ * remove this magic header.
+ */
+ protected static final byte[] JSON_MAGIC;
+
+ static {
+ try {
+ JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 not supported", e);
+ }
+ }
+
+ private final Provider<CurrentUser> currentUser;
+
+ @Inject
+ protected RestApiServlet(final Provider<CurrentUser> currentUser) {
+ this.currentUser = currentUser;
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
+ res.setHeader("Pragma", "no-cache");
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
+ res.setHeader("Content-Disposition", "attachment");
+
+ try {
+ checkRequiresCapability();
+ super.service(req, res);
+ } catch (RequireCapabilityException err) {
+ sendError(res, SC_FORBIDDEN, err.getMessage());
+ } catch (Error err) {
+ handleException(err, req, res);
+ } catch (RuntimeException err) {
+ handleException(err, req, res);
+ }
+ }
+
+ private void checkRequiresCapability() throws RequireCapabilityException {
+ RequiresCapability rc = getClass().getAnnotation(RequiresCapability.class);
+ if (rc != null) {
+ CurrentUser user = currentUser.get();
+ CapabilityControl ctl = user.getCapabilities();
+ if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+ String msg = String.format(
+ "fatal: %s does not have \"%s\" capability.",
+ Objects.firstNonNull(
+ user.getUserName(),
+ user instanceof IdentifiedUser
+ ? ((IdentifiedUser) user).getNameEmail()
+ : user.toString()),
+ rc.value());
+ throw new RequireCapabilityException(msg);
+ }
+ }
+ }
+
+ private static void handleException(Throwable err, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ String uri = req.getRequestURI();
+ if (!Strings.isNullOrEmpty(req.getQueryString())) {
+ uri += "?" + req.getQueryString();
+ }
+ log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+
+ if (!res.isCommitted()) {
+ res.reset();
+ sendError(res, SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
+ }
+ }
+
+ protected static void sendError(HttpServletResponse res,
+ int statusCode, String msg) throws IOException {
+ res.setStatus(statusCode);
+ sendText(null, res, msg);
+ }
+
+ protected static boolean acceptsJson(HttpServletRequest req) {
+ String accept = req.getHeader("Accept");
+ if (accept == null) {
+ return false;
+ } else if (JSON_TYPE.equals(accept)) {
+ return true;
+ } else if (accept.startsWith(JSON_TYPE + ",")) {
+ return true;
+ }
+ for (String p : accept.split("[ ,;][ ,;]*")) {
+ if (JSON_TYPE.equals(p)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected static void sendText(@Nullable HttpServletRequest req,
+ HttpServletResponse res, String data) throws IOException {
+ res.setContentType("text/plain");
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, data.getBytes("UTF-8"));
+ }
+
+ protected static void send(@Nullable HttpServletRequest req,
+ HttpServletResponse res, byte[] data) throws IOException {
+ if (data.length > 256 && req != null
+ && RPCServletUtils.acceptsGzipEncoding(req)) {
+ res.setHeader("Content-Encoding", "gzip");
+ data = HtmlDomUtil.compress(data);
+ }
+ res.setContentLength(data.length);
+ OutputStream out = res.getOutputStream();
+ try {
+ out.write(data);
+ } finally {
+ out.close();
+ }
+ }
+
+ public static class ParameterParser {
+ private final CmdLineParser.Factory parserFactory;
+
+ @Inject
+ ParameterParser(CmdLineParser.Factory pf) {
+ this.parserFactory = pf;
+ }
+
+ public <T> boolean parse(T param, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ return parse(param, req, res, Collections.<String>emptySet());
+ }
+
+ public <T> boolean parse(T param, HttpServletRequest req,
+ HttpServletResponse res, Set<String> argNames) throws IOException {
+ CmdLineParser clp = parserFactory.create(param);
+ try {
+ @SuppressWarnings("unchecked")
+ Map<String, String[]> parameterMap = req.getParameterMap();
+ clp.parseOptionMap(parameterMap, argNames);
+ } catch (CmdLineException e) {
+ if (!clp.wasHelpRequestedByOption()) {
+ res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ sendText(req, res, e.getMessage());
+ return false;
+ }
+ }
+
+ if (clp.wasHelpRequestedByOption()) {
+ StringWriter msg = new StringWriter();
+ clp.printQueryStringUsage(req.getRequestURI(), msg);
+ msg.write('\n');
+ msg.write('\n');
+ clp.printUsage(msg, null);
+ msg.write('\n');
+ sendText(req, res, msg.toString());
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ @SuppressWarnings("serial") // Never serialized or thrown out of this class.
+ private static class RequireCapabilityException extends Exception {
+ public RequireCapabilityException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
new file mode 100644
index 0000000000..783ebc70a9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
@@ -0,0 +1,58 @@
+// 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.httpd;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
+
+/** Verifies the token sent by {@link RegisterNewEmailSender}. */
+public interface RestTokenVerifier {
+ /**
+ * Construct a token to verify a REST PUT request.
+ *
+ * @param user the caller that wants to make a PUT request
+ * @param url the URL being requested
+ * @return an unforgeable string to send to the user as the body of a GET
+ * request. Presenting the string in a follow-up POST request provides
+ * proof the user has the ability to read messages sent to thier
+ * browser and they likely aren't making the request via XSRF.
+ */
+ public String sign(Account.Id user, String url);
+
+ /**
+ * Decode a token previously created.
+ *
+ * @param user the user making the verify request.
+ * @param url the url user is attempting to access.
+ * @param token the string created by sign.
+ * @throws InvalidTokenException the token is invalid, expired, malformed,
+ * etc.
+ */
+ public void verify(Account.Id user, String url, String token)
+ throws InvalidTokenException;
+
+ /** Exception thrown when a token does not parse correctly. */
+ public static class InvalidTokenException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidTokenException() {
+ super("Invalid token");
+ }
+
+ public InvalidTokenException(Throwable cause) {
+ super("Invalid token", cause);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
new file mode 100644
index 0000000000..83d6caa36b
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
@@ -0,0 +1,97 @@
+// 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.httpd;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.gwtjsonrpc.server.ValidToken;
+import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+
+/** Verifies the token sent by {@link RestApiServlet}. */
+public class SignedTokenRestTokenVerifier implements RestTokenVerifier {
+ private final SignedToken restToken;
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(RestTokenVerifier.class).to(SignedTokenRestTokenVerifier.class);
+ }
+ }
+
+ @Inject
+ SignedTokenRestTokenVerifier(AuthConfig config) {
+ restToken = config.getRestToken();
+ }
+
+ @Override
+ public String sign(Account.Id user, String url) {
+ try {
+ String payload = String.format("%s:%s", user, url);
+ byte[] utf8 = payload.getBytes("UTF-8");
+ String base64 = Base64.encodeBytes(utf8);
+ return restToken.newToken(base64);
+ } catch (XsrfException e) {
+ throw new IllegalArgumentException(e);
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public void verify(Account.Id user, String url, String tokenString)
+ throws InvalidTokenException {
+ ValidToken token;
+ try {
+ token = restToken.checkToken(tokenString, null);
+ } catch (XsrfException err) {
+ throw new InvalidTokenException(err);
+ }
+ if (token == null || token.getData() == null || token.getData().isEmpty()) {
+ throw new InvalidTokenException();
+ }
+
+ String payload;
+ try {
+ payload = new String(Base64.decode(token.getData()), "UTF-8");
+ } catch (UnsupportedEncodingException err) {
+ throw new InvalidTokenException(err);
+ }
+
+ int colonPos = payload.indexOf(':');
+ if (colonPos == -1) {
+ throw new InvalidTokenException();
+ }
+
+ Account.Id tokenUser;
+ try {
+ tokenUser = Account.Id.parse(payload.substring(0, colonPos));
+ } catch (IllegalArgumentException err) {
+ throw new InvalidTokenException(err);
+ }
+
+ String tokenUrl = payload.substring(colonPos+1);
+
+ if (!tokenUser.equals(user) || !tokenUrl.equals(url)) {
+ throw new InvalidTokenException();
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
new file mode 100644
index 0000000000..98a1b57463
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
@@ -0,0 +1,263 @@
+// 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.httpd;
+
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
+import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Enumeration;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class TokenVerifiedRestApiServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private static final String FORM_ENCODED = "application/x-www-form-urlencoded";
+ private static final String UTF_8 = "UTF-8";
+ private static final String AUTHKEY_NAME = "_authkey";
+ private static final String AUTHKEY_HEADER = "X-authkey";
+
+ private final Gson gson;
+ private final Provider<CurrentUser> userProvider;
+ private final RestTokenVerifier verifier;
+
+ @Inject
+ protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider,
+ RestTokenVerifier verifier) {
+ super(userProvider);
+ this.gson = OutputFormat.JSON_COMPACT.newGson();
+ this.userProvider = userProvider;
+ this.verifier = verifier;
+ }
+
+ /**
+ * Process the (possibly state changing) request.
+ *
+ * @param req incoming HTTP request.
+ * @param res outgoing response.
+ * @param requestData JSON object representing the HTTP request parameters.
+ * Null if the request body was not supplied in JSON format.
+ * @throws IOException
+ * @throws ServletException
+ */
+ protected abstract void doRequest(HttpServletRequest req,
+ HttpServletResponse res,
+ @Nullable JsonObject requestData) throws IOException, ServletException;
+
+ @Override
+ protected final void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ CurrentUser user = userProvider.get();
+ if (!(user instanceof IdentifiedUser)) {
+ sendError(res, SC_UNAUTHORIZED, "API requires authentication");
+ return;
+ }
+
+ TokenInfo info = new TokenInfo();
+ info._authkey = verifier.sign(
+ ((IdentifiedUser) user).getAccountId(),
+ computeUrl(req));
+
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ String type;
+ buf.write(JSON_MAGIC);
+ if (acceptsJson(req)) {
+ type = JSON_TYPE;
+ buf.write(gson.toJson(info).getBytes(UTF_8));
+ } else {
+ type = FORM_ENCODED;
+ buf.write(String.format("%s=%s",
+ AUTHKEY_NAME,
+ URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8));
+ }
+
+ res.setContentType(type);
+ res.setCharacterEncoding(UTF_8);
+ res.setHeader("Content-Disposition", "attachment");
+ send(req, res, buf.toByteArray());
+ }
+
+ @Override
+ protected final void doPost(HttpServletRequest req, HttpServletResponse res)
+ throws IOException, ServletException {
+ CurrentUser user = userProvider.get();
+ if (!(user instanceof IdentifiedUser)) {
+ sendError(res, SC_UNAUTHORIZED, "API requires authentication");
+ return;
+ }
+
+ ParsedBody body;
+ if (JSON_TYPE.equals(req.getContentType())) {
+ body = parseJson(req, res);
+ } else if (FORM_ENCODED.equals(req.getContentType())) {
+ body = parseForm(req, res);
+ } else {
+ sendError(res, SC_BAD_REQUEST, String.format(
+ "Expected Content-Type: %s or %s",
+ JSON_TYPE, FORM_ENCODED));
+ return;
+ }
+
+ if (body == null) {
+ return;
+ }
+
+ if (Strings.isNullOrEmpty(body._authkey)) {
+ String h = req.getHeader(AUTHKEY_HEADER);
+ if (Strings.isNullOrEmpty(h)) {
+ sendError(res, SC_BAD_REQUEST, String.format(
+ "Expected %s in request body or %s in HTTP headers",
+ AUTHKEY_NAME, AUTHKEY_HEADER));
+ return;
+ }
+ body._authkey = URLDecoder.decode(h, UTF_8);
+ }
+
+ try {
+ verifier.verify(
+ ((IdentifiedUser) user).getAccountId(),
+ computeUrl(req),
+ body._authkey);
+ } catch (InvalidTokenException err) {
+ sendError(res, SC_BAD_REQUEST,
+ String.format("Invalid or expired %s", AUTHKEY_NAME));
+ return;
+ }
+
+ doRequest(body.req, res, body.json);
+ }
+
+ private static ParsedBody parseJson(HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ try {
+ JsonElement element = new JsonParser().parse(req.getReader());
+ if (!element.isJsonObject()) {
+ sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body");
+ return null;
+ }
+
+ ParsedBody body = new ParsedBody();
+ body.req = req;
+ body.json = (JsonObject) element;
+ JsonElement authKey = body.json.remove(AUTHKEY_NAME);
+ if (authKey != null
+ && authKey.isJsonPrimitive()
+ && authKey.getAsJsonPrimitive().isString()) {
+ body._authkey = authKey.getAsString();
+ }
+ return body;
+ } catch (JsonParseException e) {
+ sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body");
+ return null;
+ }
+ }
+
+ private static ParsedBody parseForm(HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ ParsedBody body = new ParsedBody();
+ body.req = new WrappedRequest(req);
+ body._authkey = req.getParameter(AUTHKEY_NAME);
+ return body;
+ }
+
+ private static String computeUrl(HttpServletRequest req) {
+ StringBuffer url = req.getRequestURL();
+ String qs = req.getQueryString();
+ if (!Strings.isNullOrEmpty(qs)) {
+ url.append('?').append(qs);
+ }
+ return url.toString();
+ }
+
+ private static class TokenInfo {
+ String _authkey;
+ }
+
+ private static class ParsedBody {
+ HttpServletRequest req;
+ String _authkey;
+ JsonObject json;
+ }
+
+ private static class WrappedRequest extends HttpServletRequestWrapper {
+ @SuppressWarnings("rawtypes")
+ private Map parameters;
+
+ WrappedRequest(HttpServletRequest req) {
+ super(req);
+ }
+
+ @Override
+ public String getParameter(String name) {
+ if (AUTHKEY_NAME.equals(name)) {
+ return null;
+ }
+ return super.getParameter(name);
+ }
+
+ @Override
+ public String[] getParameterValues(String name) {
+ if (AUTHKEY_NAME.equals(name)) {
+ return null;
+ }
+ return super.getParameterValues(name);
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public Map getParameterMap() {
+ Map m = parameters;
+ if (m == null) {
+ m = super.getParameterMap();
+ if (m.containsKey(AUTHKEY_NAME)) {
+ m = Maps.newHashMap(m);
+ m.remove(AUTHKEY_NAME);
+ }
+ parameters = m;
+ }
+ return m;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public Enumeration getParameterNames() {
+ return Iterators.asEnumeration(getParameterMap().keySet().iterator());
+ }
+ }
+}
+
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 f90c20dd8d..a7fde9b3de 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
@@ -16,6 +16,7 @@ package com.google.gerrit.httpd;
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.base.Strings;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.httpd.raw.CatServlet;
import com.google.gerrit.httpd.raw.HostPageServlet;
@@ -23,14 +24,22 @@ import com.google.gerrit.httpd.raw.LegacyGerritServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.StaticServlet;
import com.google.gerrit.httpd.raw.ToolServlet;
+import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
+import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
+import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtexpui.server.CacheControlFilter;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
+import org.eclipse.jgit.lib.Config;
+
import java.io.IOException;
import javax.servlet.http.HttpServlet;
@@ -38,6 +47,21 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
class UrlModule extends ServletModule {
+ static class UrlConfig {
+ private final boolean deprecatedQuery;
+
+ @Inject
+ UrlConfig(@GerritServerConfig Config cfg) {
+ deprecatedQuery = cfg.getBoolean("site", "enableDeprecatedQuery", true);
+ }
+ }
+
+ private final UrlConfig cfg;
+
+ UrlModule(UrlConfig cfg) {
+ this.cfg = cfg;
+ }
+
@Override
protected void configureServlets() {
filter("/*").through(Key.get(CacheControlFilter.class));
@@ -48,7 +72,6 @@ class UrlModule extends ServletModule {
serve("/Gerrit/*").with(legacyGerritScreen());
serve("/cat/*").with(CatServlet.class);
serve("/logout").with(HttpLogoutServlet.class);
- serve("/query").with(ChangeQueryServlet.class);
serve("/signout").with(HttpLogoutServlet.class);
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
@@ -69,6 +92,15 @@ class UrlModule extends ServletModule {
serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
serveRegex("^/p/(.*)$").with(queryProjectNew());
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
+
+ filter("/a/*").through(RequireIdentifiedUserFilter.class);
+ serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
+ serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
+ serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
+
+ if (cfg.deprecatedQuery) {
+ serve("/query").with(DeprecatedChangeQueryServlet.class);
+ }
}
private Key<HttpServlet> notFound() {
@@ -133,6 +165,11 @@ class UrlModule extends ServletModule {
protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
String name = req.getPathInfo();
+ if (Strings.isNullOrEmpty(name)) {
+ toGerrit(PageLinks.ADMIN_PROJECTS, req, rsp);
+ return;
+ }
+
while (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 8ee2c41447..1a48bb5841 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -15,6 +15,7 @@
package com.google.gerrit.httpd;
import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
@@ -23,8 +24,8 @@ import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
import com.google.gerrit.httpd.gitweb.GitWebModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
@@ -51,14 +52,17 @@ import javax.annotation.Nullable;
public class WebModule extends FactoryModule {
private final AuthConfig authConfig;
+ private final UrlModule.UrlConfig urlConfig;
private final boolean wantSSL;
private final GitWebConfig gitWebConfig;
@Inject
WebModule(final AuthConfig authConfig,
+ final UrlModule.UrlConfig urlConfig,
@CanonicalWebUrl @Nullable final String canonicalUrl,
final Injector creatingInjector) {
this.authConfig = authConfig;
+ this.urlConfig = urlConfig;
this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
this.gitWebConfig =
@@ -72,13 +76,8 @@ public class WebModule extends FactoryModule {
@Override
protected void configure() {
- install(new ServletModule() {
- @Override
- protected void configureServlets() {
- filter("/*").through(RequestCleanupFilter.class);
- }
- });
bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
+ bind(HttpRequestContext.class);
if (wantSSL) {
install(new RequireSslFilter.Module());
@@ -109,6 +108,7 @@ public class WebModule extends FactoryModule {
break;
case OPENID:
+ case OPENID_SSO:
// OpenID support is bound in WebAppInitializer and Daemon.
case CUSTOM_EXTENSION:
break;
@@ -116,7 +116,7 @@ public class WebModule extends FactoryModule {
throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
}
- install(new UrlModule());
+ install(new UrlModule(urlConfig));
install(new UiRpcModule());
install(new GerritRequestModule());
install(new GitOverHttpServlet.Module());
@@ -135,12 +135,17 @@ public class WebModule extends FactoryModule {
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
factory(ClearPassword.Factory.class);
+ install(new CmdLineParserModule());
factory(GeneratePassword.Factory.class);
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
HttpRemotePeerProvider.class).in(RequestScoped.class);
- bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class);
- bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class);
+ install(new LifecycleModule() {
+ @Override
+ protected void configure() {
+ listener().toInstance(registerInParentInjectors());
+ }
+ });
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 2925896f2b..44920a4831 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -16,11 +16,13 @@ package com.google.gerrit.httpd;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthResult;
public interface WebSession {
+ public AuthMethod getAuthMethod();
+
public boolean isSignedIn();
public String getToken();
@@ -31,13 +33,10 @@ public interface WebSession {
public CurrentUser getCurrentUser();
- public void login(AuthResult res, boolean rememberMe);
-
- /** Change the access path from the default of {@link AccessPath#WEB_UI}. */
- public void setAccessPath(AccessPath path);
+ public void login(AuthResult res, AuthMethod meth, boolean rememberMe);
/** Set the user account for this current request only. */
- public void setUserAccountId(Account.Id id);
+ public void setUserAccountId(Account.Id id, AuthMethod method);
public void logout();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 55d0ca58df..4b4edf4710 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -26,9 +26,9 @@ import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
+import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -43,6 +43,7 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.SecureRandom;
+import java.util.concurrent.TimeUnit;
@Singleton
class WebSessionManager {
@@ -54,11 +55,11 @@ class WebSessionManager {
private final long sessionMaxAgeMillis;
private final SecureRandom prng;
- private final Cache<Key, Val> self;
+ private final Cache<String, Val> self;
@Inject
WebSessionManager(@GerritServerConfig Config cfg,
- @Named(CACHE_NAME) final Cache<Key, Val> cache) {
+ @Named(CACHE_NAME) final Cache<String, Val> cache) {
prng = new SecureRandom();
self = cache;
@@ -75,7 +76,7 @@ class WebSessionManager {
prng.nextBytes(rnd);
buf = new ByteArrayOutputStream(3 + nonceLen);
- writeVarInt32(buf, (int) Key.serialVersionUID);
+ writeVarInt32(buf, (int) Val.serialVersionUID);
writeVarInt32(buf, who.get());
writeBytes(buf, rnd);
@@ -104,7 +105,9 @@ class WebSessionManager {
final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
final long minRefresh = MILLISECONDS.convert(1, HOURS);
final long refresh = Math.min(halfAgeRefresh, minRefresh);
- final long refreshCookieAt = now() + refresh;
+ final long now = now();
+ final long refreshCookieAt = now + refresh;
+ final long expiresAt = now + sessionMaxAgeMillis;
if (xsrfToken == null) {
// If we don't yet have a token for this session, establish one.
@@ -115,8 +118,9 @@ class WebSessionManager {
xsrfToken = CookieBase64.encode(rnd);
}
- Val val = new Val(who, refreshCookieAt, remember, lastLogin, xsrfToken);
- self.put(key, val);
+ Val val = new Val(who, refreshCookieAt, remember,
+ lastLogin, xsrfToken, expiresAt);
+ self.put(key.token, val);
return val;
}
@@ -137,16 +141,19 @@ class WebSessionManager {
}
Val get(final Key key) {
- return self.get(key);
+ Val val = self.getIfPresent(key.token);
+ if (val != null && val.expiresAt <= now()) {
+ self.invalidate(key.token);
+ return null;
+ }
+ return val;
}
void destroy(final Key key) {
- self.remove(key);
+ self.invalidate(key.token);
}
- static final class Key implements Serializable {
- static final long serialVersionUID = 2L;
-
+ static final class Key {
private transient String token;
Key(final String t) {
@@ -166,33 +173,28 @@ class WebSessionManager {
public boolean equals(Object obj) {
return obj instanceof Key && token.equals(((Key) obj).token);
}
-
- private void writeObject(final ObjectOutputStream out) throws IOException {
- writeString(out, token);
- }
-
- private void readObject(final ObjectInputStream in) throws IOException {
- token = readString(in);
- }
}
static final class Val implements Serializable {
- static final long serialVersionUID = Key.serialVersionUID;
+ static final long serialVersionUID = 2L;
private transient Account.Id accountId;
private transient long refreshCookieAt;
private transient boolean persistentCookie;
private transient AccountExternalId.Key externalId;
private transient String xsrfToken;
+ private transient long expiresAt;
Val(final Account.Id accountId, final long refreshCookieAt,
final boolean persistentCookie, final AccountExternalId.Key externalId,
- final String xsrfToken) {
+ final String xsrfToken,
+ final long expiresAt) {
this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie;
this.externalId = externalId;
this.xsrfToken = xsrfToken;
+ this.expiresAt = expiresAt;
}
Account.Id getAccountId() {
@@ -233,6 +235,9 @@ class WebSessionManager {
writeVarInt32(out, 5);
writeString(out, xsrfToken);
+ writeVarInt32(out, 6);
+ writeFixInt64(out, expiresAt);
+
writeVarInt32(out, 0);
}
@@ -257,10 +262,16 @@ class WebSessionManager {
case 5:
xsrfToken = readString(in);
continue;
+ case 6:
+ expiresAt = readFixInt64(in);
+ continue;
default:
throw new IOException("Unknown tag found in object: " + tag);
}
}
+ if (expiresAt == 0) {
+ expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
+ }
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 4710c39705..0821496159 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -24,6 +24,7 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gwtorm.server.OrmException;
@@ -113,7 +114,7 @@ public class BecomeAnyAccountLoginServlet extends HttpServlet {
}
if (res != null) {
- webSession.get().login(res, false);
+ webSession.get().login(res, AuthMethod.BACKDOOR, false);
final StringBuilder rdr = new StringBuilder();
rdr.append(req.getContextPath());
if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 5df004e972..9b7eaf5e47 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -19,6 +19,7 @@ import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.config.AuthConfig;
@@ -135,7 +136,8 @@ class HttpLoginServlet extends HttpServlet {
}
rdr.append(token);
- webSession.get().login(arsp, true /* persistent cookie */);
+ webSession.get().login(arsp, AuthMethod.COOKIE,
+ true /* persistent cookie */);
rsp.sendRedirect(rdr.toString());
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 381daa8c87..ff0eb29f58 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -17,6 +17,7 @@ package com.google.gerrit.httpd.auth.container;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.inject.Inject;
@@ -84,7 +85,7 @@ class HttpsClientSslCertAuthFilter implements Filter {
log.error(err, e);
throw new ServletException(err, e);
}
- webSession.get().login(arsp, true);
+ webSession.get().login(arsp, AuthMethod.COOKIE, true);
chain.doFilter(req, rsp);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
index 9d14872d3a..348ecbb9b2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
@@ -21,6 +21,7 @@ import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountUserNameException;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
@@ -29,11 +30,17 @@ import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
class UserPassAuthServiceImpl implements UserPassAuthService {
private final Provider<WebSession> webSession;
private final AccountManager accountManager;
private final AuthType authType;
+ private static final Logger log = LoggerFactory
+ .getLogger(UserPassAuthServiceImpl.class);
+
@Inject
UserPassAuthServiceImpl(final Provider<WebSession> webSession,
final AccountManager accountManager, final AuthConfig authConfig) {
@@ -72,6 +79,7 @@ class UserPassAuthServiceImpl implements UserPassAuthService {
callback.onSuccess(result);
return;
} catch (AccountException e) {
+ log.info(String.format("'%s' failed to sign in: %s", username, e.getMessage()));
result.setError(LoginResult.Error.INVALID_LOGIN);
callback.onSuccess(result);
return;
@@ -79,7 +87,8 @@ class UserPassAuthServiceImpl implements UserPassAuthService {
result.success = true;
result.isNew = res.isNew();
- webSession.get().login(res, true /* persistent cookie */);
+ webSession.get().login(res, AuthMethod.PASSWORD,
+ true /* persistent cookie */);
callback.onSuccess(result);
}
}
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 fb0422672b..6c37e43945 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
@@ -370,7 +370,7 @@ class GitWebServlet extends HttpServlet {
final ProjectControl project;
try {
project = projectControl.validateFor(nameKey);
- if (!project.allRefsAreVisible()) {
+ if (!project.allRefsAreVisible() && !project.isOwner()) {
// Pretend the project doesn't exist
throw new NoSuchProjectException(nameKey);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000000..2d957f275f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -0,0 +1,69 @@
+// 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+class HttpAutoRegisterModuleGenerator extends ServletModule
+ implements ModuleGenerator {
+ private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+
+ @Override
+ protected void configureServlets() {
+ for (Map.Entry<String, Class<HttpServlet>> e : serve.entrySet()) {
+ bind(e.getValue()).in(Scopes.SINGLETON);
+ serve(e.getKey()).with(e.getValue());
+ }
+ }
+
+ @Override
+ public void setPluginName(String name) {
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ if (HttpServlet.class.isAssignableFrom(type)) {
+ Class<HttpServlet> old = serve.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ serve.put(export.value(), (Class<HttpServlet>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s",
+ type.getName(), export.value(),
+ HttpServlet.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ return this;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
new file mode 100644
index 0000000000..2bcaa30310
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.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.httpd.plugins;
+
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.servlet.ServletModule;
+
+public class HttpPluginModule extends ServletModule {
+ static final String PLUGIN_RESOURCES = "plugin_resources";
+
+ @Override
+ protected void configureServlets() {
+ bind(HttpPluginServlet.class);
+ serve("/plugins/*").with(HttpPluginServlet.class);
+ serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
+
+ bind(StartPluginListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(HttpPluginServlet.class);
+
+ bind(ReloadPluginListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(HttpPluginServlet.class);
+
+ bind(ModuleGenerator.class)
+ .to(HttpAutoRegisterModuleGenerator.class);
+
+ install(new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(PLUGIN_RESOURCES, ResourceKey.class, Resource.class)
+ .maximumWeight(2 << 20)
+ .weigher(ResourceWeigher.class);
+ }
+ });
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
new file mode 100644
index 0000000000..e737700088
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -0,0 +1,596 @@
+// 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.httpd.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.rpc.plugin.ListPluginsServlet;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.documentation.MarkdownFormatter;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.servlet.GuiceFilter;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class HttpPluginServlet extends HttpServlet
+ implements StartPluginListener, ReloadPluginListener {
+ private static final int SMALL_RESOURCE = 128 * 1024;
+ private static final long serialVersionUID = 1L;
+ private static final Logger log
+ = LoggerFactory.getLogger(HttpPluginServlet.class);
+
+ private final MimeUtilFileTypeRegistry mimeUtil;
+ private final Provider<String> webUrl;
+ private final Cache<ResourceKey, Resource> resourceCache;
+ private final String sshHost;
+ private final int sshPort;
+ private final ListPluginsServlet listServlet;
+
+ private List<Plugin> pending = Lists.newArrayList();
+ private String base;
+ private final ConcurrentMap<String, PluginHolder> plugins
+ = Maps.newConcurrentMap();
+
+ @Inject
+ HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil,
+ @CanonicalWebUrl Provider<String> webUrl,
+ @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
+ @GerritServerConfig Config cfg,
+ SshInfo sshInfo, ListPluginsServlet listServlet) {
+ this.mimeUtil = mimeUtil;
+ this.webUrl = webUrl;
+ this.resourceCache = cache;
+ this.listServlet = listServlet;
+
+ String sshHost = "review.example.com";
+ int sshPort = 29418;
+ if (!sshInfo.getHostKeys().isEmpty()) {
+ String host = sshInfo.getHostKeys().get(0).getHost();
+ int c = host.lastIndexOf(':');
+ if (0 <= c) {
+ sshHost = host.substring(0, c);
+ sshPort = Integer.parseInt(host.substring(c+1));
+ } else {
+ sshHost = host;
+ sshPort = 22;
+ }
+ }
+ this.sshHost = sshHost;
+ this.sshPort = sshPort;
+ }
+
+ @Override
+ public synchronized void init(ServletConfig config) throws ServletException {
+ super.init(config);
+
+ String path = config.getServletContext().getContextPath();
+ base = Strings.nullToEmpty(path) + "/plugins/";
+ for (Plugin plugin : pending) {
+ install(plugin);
+ }
+ pending = null;
+ }
+
+ @Override
+ public synchronized void onStartPlugin(Plugin plugin) {
+ if (pending != null) {
+ pending.add(plugin);
+ } else {
+ install(plugin);
+ }
+ }
+
+ @Override
+ public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+ install(newPlugin);
+ }
+
+ private void install(Plugin plugin) {
+ GuiceFilter filter = load(plugin);
+ final String name = plugin.getName();
+ final PluginHolder holder = new PluginHolder(plugin, filter);
+ plugin.add(new RegistrationHandle() {
+ @Override
+ public void remove() {
+ plugins.remove(name, holder);
+ }
+ });
+ plugins.put(name, holder);
+ }
+
+ private GuiceFilter load(Plugin plugin) {
+ if (plugin.getHttpInjector() != null) {
+ final String name = plugin.getName();
+ final GuiceFilter filter;
+ try {
+ filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
+ } catch (RuntimeException e) {
+ log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
+ return null;
+ }
+
+ try {
+ WrappedContext ctx = new WrappedContext(plugin, base + name);
+ filter.init(new WrappedFilterConfig(ctx));
+ } catch (ServletException e) {
+ log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
+ return null;
+ }
+
+ plugin.add(new RegistrationHandle() {
+ @Override
+ public void remove() {
+ filter.destroy();
+ }
+ });
+ return filter;
+ }
+ return null;
+ }
+
+ @Override
+ public void service(HttpServletRequest req, HttpServletResponse res)
+ throws IOException, ServletException {
+ String name = extractName(req);
+ if (name.equals("")) {
+ listServlet.service(req, res);
+ return;
+ }
+ final PluginHolder holder = plugins.get(name);
+ if (holder == null) {
+ noCache(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+
+ WrappedRequest wr = new WrappedRequest(req, base + name);
+ FilterChain chain = new FilterChain() {
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res)
+ throws IOException {
+ onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
+ }
+ };
+ if (holder.filter != null) {
+ holder.filter.doFilter(wr, res, chain);
+ } else {
+ chain.doFilter(wr, res);
+ }
+ }
+
+ private void onDefault(PluginHolder holder,
+ HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
+ noCache(res);
+ res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ return;
+ }
+
+ String uri = req.getRequestURI();
+ String ctx = req.getContextPath();
+ if (uri.length() <= ctx.length()) {
+ Resource.NOT_FOUND.send(req, res);
+ return;
+ }
+
+ String file = uri.substring(ctx.length() + 1);
+ ResourceKey key = new ResourceKey(holder.plugin, file);
+ Resource rsc = resourceCache.getIfPresent(key);
+ if (rsc != null) {
+ rsc.send(req, res);
+ return;
+ }
+
+ if ("".equals(file)) {
+ res.sendRedirect(uri + "Documentation/index.html");
+ } else if (file.startsWith("static/")) {
+ JarFile jar = holder.plugin.getJarFile();
+ JarEntry entry = jar.getJarEntry(file);
+ if (exists(entry)) {
+ sendResource(jar, entry, key, res);
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ } else if (file.equals("Documentation")) {
+ res.sendRedirect(uri + "/index.html");
+ } else if (file.startsWith("Documentation/") && file.endsWith("/")) {
+ res.sendRedirect(uri + "index.html");
+ } else if (file.startsWith("Documentation/")) {
+ JarFile jar = holder.plugin.getJarFile();
+ JarEntry entry = jar.getJarEntry(file);
+ if (!exists(entry)) {
+ entry = findSource(jar, file);
+ }
+ if (!exists(entry) && file.endsWith("/index.html")) {
+ String pfx = file.substring(0, file.length() - "index.html".length());
+ sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res);
+ } else if (exists(entry) && entry.getName().endsWith(".md")) {
+ sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res);
+ } else if (exists(entry)) {
+ sendResource(jar, entry, key, res);
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ }
+
+ private void sendAutoIndex(JarFile jar,
+ String prefix, String pluginName,
+ ResourceKey cacheKey, HttpServletResponse res) throws IOException {
+ List<JarEntry> cmds = Lists.newArrayList();
+ List<JarEntry> docs = Lists.newArrayList();
+ Enumeration<JarEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ long size = entry.getSize();
+ if (name.startsWith(prefix)
+ && (name.endsWith(".md")
+ || name.endsWith(".html"))
+ && 0 < size && size <= SMALL_RESOURCE) {
+ if (name.substring(prefix.length()).startsWith("cmd-")) {
+ cmds.add(entry);
+ } else {
+ docs.add(entry);
+ }
+ }
+ }
+ Collections.sort(cmds, new Comparator<JarEntry>() {
+ @Override
+ public int compare(JarEntry a, JarEntry b) {
+ return a.getName().compareTo(b.getName());
+ }
+ });
+ Collections.sort(docs, new Comparator<JarEntry>() {
+ @Override
+ public int compare(JarEntry a, JarEntry b) {
+ return a.getName().compareTo(b.getName());
+ }
+ });
+
+ StringBuilder md = new StringBuilder();
+ md.append(String.format("# Plugin %s #\n", pluginName));
+ md.append("\n");
+ appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
+
+ if (!docs.isEmpty()) {
+ md.append("## Documentation ##\n");
+ for(JarEntry entry : docs) {
+ String rsrc = entry.getName().substring(prefix.length());
+ String title;
+ if (rsrc.endsWith(".html")) {
+ title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
+ } else if (rsrc.endsWith(".md")) {
+ title = extractTitleFromMarkdown(jar, entry);
+ if (Strings.isNullOrEmpty(title)) {
+ title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
+ }
+ rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ } else {
+ title = rsrc.replace('-', ' ');
+ }
+ md.append(String.format("* [%s](%s)\n", title, rsrc));
+ }
+ md.append("\n");
+ }
+
+ if (!cmds.isEmpty()) {
+ md.append("## Commands ##\n");
+ for(JarEntry entry : cmds) {
+ String rsrc = entry.getName().substring(prefix.length());
+ String title;
+ if (rsrc.endsWith(".html")) {
+ title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
+ } else if (rsrc.endsWith(".md")) {
+ title = extractTitleFromMarkdown(jar, entry);
+ if (Strings.isNullOrEmpty(title)) {
+ title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
+ }
+ rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ } else {
+ title = rsrc.substring(4).replace('-', ' ');
+ }
+ md.append(String.format("* [%s](%s)\n", title, rsrc));
+ }
+ md.append("\n");
+ }
+
+ sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
+ }
+
+ private void sendMarkdownAsHtml(String md, String pluginName,
+ ResourceKey cacheKey, HttpServletResponse res)
+ throws UnsupportedEncodingException, IOException {
+ Map<String, String> macros = Maps.newHashMap();
+ macros.put("PLUGIN", pluginName);
+ macros.put("SSH_HOST", sshHost);
+ macros.put("SSH_PORT", "" + sshPort);
+ String url = webUrl.get();
+ if (Strings.isNullOrEmpty(url)) {
+ url = "http://review.example.com/";
+ }
+ macros.put("URL", url);
+
+ Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String key = m.group(2);
+ String val = macros.get(key);
+ if (m.group(1) != null) {
+ m.appendReplacement(sb, "@" + key + "@");
+ } else if (val != null) {
+ m.appendReplacement(sb, val);
+ } else {
+ m.appendReplacement(sb, "@" + key + "@");
+ }
+ }
+ m.appendTail(sb);
+
+ byte[] html = new MarkdownFormatter()
+ .markdownToDocHtml(sb.toString(), "UTF-8");
+ resourceCache.put(cacheKey, new SmallResource(html)
+ .setContentType("text/html")
+ .setCharacterEncoding("UTF-8"));
+ res.setContentType("text/html");
+ res.setCharacterEncoding("UTF-8");
+ res.setContentLength(html.length);
+ res.getOutputStream().write(html);
+ }
+
+ private static void appendPluginInfoTable(StringBuilder html, Attributes main) {
+ if (main != null) {
+ String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
+ String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
+ String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
+ String a = main.getValue("Gerrit-ApiVersion");
+
+ html.append("<table class=\"plugin_info\">");
+ if (!Strings.isNullOrEmpty(t)) {
+ html.append("<tr><th>Name</th><td>")
+ .append(t)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(n)) {
+ html.append("<tr><th>Vendor</th><td>")
+ .append(n)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(v)) {
+ html.append("<tr><th>Version</th><td>")
+ .append(v)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(u)) {
+ html.append("<tr><th>URL</th><td>")
+ .append(String.format("<a href=\"%s\">%s</a>", u, u))
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(a)) {
+ html.append("<tr><th>API Version</th><td>")
+ .append(a)
+ .append("</td></tr>\n");
+ }
+ html.append("</table>\n");
+ }
+ }
+
+ private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry)
+ throws IOException {
+ String charEnc = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+ if (charEnc == null) {
+ charEnc = "UTF-8";
+ }
+ return new MarkdownFormatter().extractTitleFromMarkdown(
+ readWholeEntry(jar, entry),
+ charEnc);
+ }
+
+ private static JarEntry findSource(JarFile jar, String file) {
+ if (file.endsWith(".html")) {
+ int d = file.lastIndexOf('.');
+ return jar.getJarEntry(file.substring(0, d) + ".md");
+ }
+ return null;
+ }
+
+ private static boolean exists(JarEntry entry) {
+ return entry != null && entry.getSize() > 0;
+ }
+
+ private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
+ String pluginName, ResourceKey key, HttpServletResponse res)
+ throws IOException {
+ byte[] rawmd = readWholeEntry(jar, entry);
+ String encoding = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ encoding = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+
+ String txtmd = RawParseUtils.decode(
+ Charset.forName(encoding != null ? encoding : "UTF-8"),
+ rawmd);
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ sendMarkdownAsHtml(txtmd, pluginName, key, res);
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ ResourceKey key, HttpServletResponse res)
+ throws IOException {
+ byte[] data = null;
+ if (entry.getSize() <= SMALL_RESOURCE) {
+ data = readWholeEntry(jar, entry);
+ }
+
+ String contentType = null;
+ String charEnc = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+ charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+ if (contentType == null) {
+ contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
+ }
+
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ res.setContentType(contentType);
+ if (charEnc != null) {
+ res.setCharacterEncoding(charEnc);
+ }
+ if (data != null) {
+ resourceCache.put(key, new SmallResource(data)
+ .setContentType(contentType)
+ .setCharacterEncoding(charEnc)
+ .setLastModified(time));
+ res.getOutputStream().write(data);
+ } else {
+ InputStream in = jar.getInputStream(entry);
+ try {
+ OutputStream out = res.getOutputStream();
+ try {
+ byte[] tmp = new byte[1024];
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ out.write(tmp, 0, n);
+ }
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+ }
+ }
+
+ private static byte[] readWholeEntry(JarFile jar, JarEntry entry)
+ throws IOException {
+ byte[] data = new byte[(int) entry.getSize()];
+ InputStream in = jar.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ return data;
+ }
+
+ private static String extractName(HttpServletRequest req) {
+ String path = req.getPathInfo();
+ if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
+ return "";
+ }
+ int s = path.indexOf('/', 1);
+ return 0 <= s ? path.substring(1, s) : path.substring(1);
+ }
+
+ static void noCache(HttpServletResponse res) {
+ res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
+ res.setHeader("Pragma", "no-cache");
+ res.setHeader("Cache-Control", "no-cache, must-revalidate");
+ res.setHeader("Content-Disposition", "attachment");
+ }
+
+ private static class PluginHolder {
+ final Plugin plugin;
+ final GuiceFilter filter;
+
+ PluginHolder(Plugin plugin, GuiceFilter filter) {
+ this.plugin = plugin;
+ this.filter = filter;
+ }
+ }
+
+ private static class WrappedRequest extends HttpServletRequestWrapper {
+ private final String contextPath;
+
+ WrappedRequest(HttpServletRequest req, String contextPath) {
+ super(req);
+ this.contextPath = contextPath;
+ }
+
+ @Override
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ @Override
+ public String getServletPath() {
+ return ((HttpServletRequest) getRequest()).getRequestURI();
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
new file mode 100644
index 0000000000..05970afa6f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
@@ -0,0 +1,40 @@
+// 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.httpd.plugins;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+abstract class Resource {
+ static final Resource NOT_FOUND = new Resource() {
+ @Override
+ int weigh() {
+ return 0;
+ }
+
+ @Override
+ void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ HttpPluginServlet.noCache(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+ };
+
+ abstract int weigh();
+ abstract void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException;
+} \ No newline at end of file
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
new file mode 100644
index 0000000000..068d6b4fd6
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
@@ -0,0 +1,45 @@
+// 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.httpd.plugins;
+
+import com.google.gerrit.server.plugins.Plugin;
+
+final class ResourceKey {
+ private final Plugin.CacheKey plugin;
+ private final String resource;
+
+ ResourceKey(Plugin p, String r) {
+ this.plugin = p.getCacheKey();
+ this.resource = r;
+ }
+
+ int weigh() {
+ return resource.length() * 2;
+ }
+
+ @Override
+ public int hashCode() {
+ return plugin.hashCode() * 31 + resource.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ResourceKey) {
+ ResourceKey rk = (ResourceKey) other;
+ return plugin == rk.plugin && resource.equals(rk.resource);
+ }
+ return false;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
new file mode 100644
index 0000000000..2514272666
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
@@ -0,0 +1,24 @@
+// 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.httpd.plugins;
+
+import com.google.common.cache.Weigher;
+
+class ResourceWeigher implements Weigher<ResourceKey, Resource> {
+ @Override
+ public int weigh(ResourceKey key, Resource value) {
+ return key.weigh() + value.weigh();
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
new file mode 100644
index 0000000000..e408f723ba
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
@@ -0,0 +1,66 @@
+// 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.httpd.plugins;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+final class SmallResource extends Resource {
+ private final byte[] data;
+ private String contentType;
+ private String characterEncoding;
+ private long lastModified;
+
+ SmallResource(byte[] data) {
+ this.data = data;
+ }
+
+ SmallResource setLastModified(long when) {
+ this.lastModified = when;
+ return this;
+ }
+
+ SmallResource setContentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ SmallResource setCharacterEncoding(@Nullable String enc) {
+ this.characterEncoding = enc;
+ return this;
+ }
+
+ @Override
+ int weigh() {
+ return contentType.length() * 2 + data.length;
+ }
+
+ @Override
+ void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ if (0 < lastModified) {
+ res.setDateHeader("Last-Modified", lastModified);
+ }
+ res.setContentType(contentType);
+ if (characterEncoding != null) {
+ res.setCharacterEncoding(characterEncoding);
+ }
+ res.setContentLength(data.length);
+ res.getOutputStream().write(data);
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
new file mode 100644
index 0000000000..daeb6ff87d
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
@@ -0,0 +1,178 @@
+// 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.plugins.Plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+class WrappedContext implements ServletContext {
+ private static final Logger log = LoggerFactory.getLogger("plugin");
+ private final Plugin plugin;
+ private final String contextPath;
+ private final ConcurrentMap<String, Object> attributes;
+
+ WrappedContext(Plugin plugin, String contextPath) {
+ this.plugin = plugin;
+ this.contextPath = contextPath;
+ this.attributes = Maps.newConcurrentMap();
+ }
+
+ @Override
+ public String getContextPath() {
+ return contextPath;
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getInitParameterNames() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @Override
+ public ServletContext getContext(String name) {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getNamedDispatcher(String name) {
+ return null;
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String name) {
+ return null;
+ }
+
+ @Override
+ public URL getResource(String name) throws MalformedURLException {
+ return null;
+ }
+
+ @Override
+ public InputStream getResourceAsStream(String name) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Set getResourcePaths(String name) {
+ return null;
+ }
+
+ @Override
+ public Servlet getServlet(String name) throws ServletException {
+ return null;
+ }
+
+ @Override
+ public String getRealPath(String name) {
+ return null;
+ }
+
+ @Override
+ public String getServletContextName() {
+ return plugin.getName();
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getServletNames() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getServlets() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @Override
+ public void log(Exception reason, String msg) {
+ log(msg, reason);
+ }
+
+ @Override
+ public void log(String msg) {
+ log(msg, null);
+ }
+
+ @Override
+ public void log(String msg, Throwable reason) {
+ log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return attributes.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getAttributeNames() {
+ return Collections.enumeration(attributes.keySet());
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ attributes.remove(name);
+ }
+
+ @Override
+ public String getMimeType(String file) {
+ return null;
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return 2;
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return 5;
+ }
+
+ @Override
+ public String getServerInfo() {
+ String v = Version.getVersion();
+ return "Gerrit Code Review/" + (v != null ? v : "dev");
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
new file mode 100644
index 0000000000..c9107dc8d2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
@@ -0,0 +1,52 @@
+// 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.httpd.plugins;
+
+import com.google.inject.servlet.GuiceFilter;
+
+import java.util.Collections;
+import java.util.Enumeration;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+class WrappedFilterConfig implements FilterConfig {
+ private final WrappedContext context;
+
+ WrappedFilterConfig(WrappedContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public String getFilterName() {
+ return GuiceFilter.class.getName();
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ return null;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Enumeration getInitParameterNames() {
+ return Collections.enumeration(Collections.emptyList());
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return context;
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
index 68379d765e..9723674d03 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
@@ -43,6 +43,9 @@ class ThemeFactory {
theme.trimColor = color(name, "trimColor", "#D4E9A9");
theme.selectionColor = color(name, "selectionColor", "#FFFFCC");
theme.topMenuColor = color(name, "topMenuColor", theme.trimColor);
+ theme.changeTableOutdatedColor = color(name, "changeTableOutdatedColor", "#F08080");
+ theme.tableOddRowColor = color(name, "tableOddRowColor", "transparent");
+ theme.tableEvenRowColor = color(name, "tableEvenRowColor", "transparent");
return theme;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 26db6f9f79..62506f0431 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Provider;
/** Support for services which require a {@link ReviewDb} instance. */
@@ -70,20 +71,14 @@ public class BaseServiceImplementation {
callback.onFailure(new NoSuchEntityException());
} catch (NoSuchGroupException e) {
callback.onFailure(new NoSuchEntityException());
-
- } catch (OrmException e) {
- if (e.getCause() instanceof Failure) {
- callback.onFailure(e.getCause().getCause());
-
- } else if (e.getCause() instanceof CorruptEntityException) {
- callback.onFailure(e.getCause());
-
- } else if (e.getCause() instanceof NoSuchEntityException) {
- callback.onFailure(e.getCause());
-
- } else {
- callback.onFailure(e);
+ } catch (OrmRuntimeException e) {
+ Exception ex = e;
+ if (e.getCause() instanceof OrmException) {
+ ex = (OrmException) e.getCause();
}
+ handleOrmException(callback, ex);
+ } catch (OrmException e) {
+ handleOrmException(callback, e);
} catch (Failure e) {
if (e.getCause() instanceof NoSuchProjectException
|| e.getCause() instanceof NoSuchChangeException) {
@@ -95,6 +90,19 @@ public class BaseServiceImplementation {
}
}
+ private static <T> void handleOrmException(
+ final AsyncCallback<T> callback, Exception e) {
+ if (e.getCause() instanceof Failure) {
+ callback.onFailure(e.getCause().getCause());
+ } else if (e.getCause() instanceof CorruptEntityException) {
+ callback.onFailure(e.getCause());
+ } else if (e.getCause() instanceof NoSuchEntityException) {
+ callback.onFailure(e.getCause());
+ } else {
+ callback.onFailure(e);
+ }
+ }
+
/** Exception whose cause is passed into onFailure. */
public static class Failure extends Exception {
private static final long serialVersionUID = 1L;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index 9a101d3ff0..0b54db1fef 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -14,259 +14,32 @@
package com.google.gerrit.httpd.rpc;
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.common.data.ChangeListService;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.SingleListChangeInfo;
import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.common.errors.InvalidQueryException;
-import com.google.gerrit.common.errors.NoSuchEntityException;
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.StarredChange;
-import com.google.gerrit.reviewdb.server.ChangeAccess;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.account.AccountInfoCacheFactory;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class ChangeListServiceImpl extends BaseServiceImplementation implements
ChangeListService {
- private static final Comparator<ChangeInfo> ID_COMP =
- new Comparator<ChangeInfo>() {
- public int compare(final ChangeInfo o1, final ChangeInfo o2) {
- return o1.getId().get() - o2.getId().get();
- }
- };
- private static final Comparator<ChangeInfo> SORT_KEY_COMP =
- new Comparator<ChangeInfo>() {
- public int compare(final ChangeInfo o1, final ChangeInfo o2) {
- return o2.getSortKey().compareTo(o1.getSortKey());
- }
- };
- private static final Comparator<Change> QUERY_PREV =
- new Comparator<Change>() {
- public int compare(final Change a, final Change b) {
- return a.getSortKey().compareTo(b.getSortKey());
- }
- };
- private static final Comparator<Change> QUERY_NEXT =
- new Comparator<Change>() {
- public int compare(final Change a, final Change b) {
- return b.getSortKey().compareTo(a.getSortKey());
- }
- };
-
private final Provider<CurrentUser> currentUser;
- private final ChangeControl.Factory changeControlFactory;
- private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
-
- private final ChangeQueryBuilder.Factory queryBuilder;
- private final Provider<ChangeQueryRewriter> queryRewriter;
@Inject
ChangeListServiceImpl(final Provider<ReviewDb> schema,
- final Provider<CurrentUser> currentUser,
- final ChangeControl.Factory changeControlFactory,
- final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
- final ChangeQueryBuilder.Factory queryBuilder,
- final Provider<ChangeQueryRewriter> queryRewriter) {
+ final Provider<CurrentUser> currentUser) {
super(schema, currentUser);
this.currentUser = currentUser;
- this.changeControlFactory = changeControlFactory;
- this.accountInfoCacheFactory = accountInfoCacheFactory;
- this.queryBuilder = queryBuilder;
- this.queryRewriter = queryRewriter;
- }
-
- private boolean canRead(final Change c, final ReviewDb db) throws OrmException {
- try {
- return changeControlFactory.controlFor(c).isVisible(db);
- } catch (NoSuchChangeException e) {
- return false;
- }
- }
-
- @Override
- public void allQueryPrev(final String query, final String pos,
- final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
- try {
- run(callback, new QueryPrev(pageSize, pos) {
- @Override
- ResultSet<Change> query(ReviewDb db, int lim, String key)
- throws OrmException, InvalidQueryException {
- return searchQuery(db, query, lim, key, QUERY_PREV);
- }
- });
- } catch (InvalidQueryException e) {
- callback.onFailure(e);
- }
- }
-
- @Override
- public void allQueryNext(final String query, final String pos,
- final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
- try {
- run(callback, new QueryNext(pageSize, pos) {
- @Override
- ResultSet<Change> query(ReviewDb db, int lim, String key)
- throws OrmException, InvalidQueryException {
- return searchQuery(db, query, lim, key, QUERY_NEXT);
- }
- });
- } catch (InvalidQueryException e) {
- callback.onFailure(e);
- }
- }
-
- @SuppressWarnings("unchecked")
- private ResultSet<Change> searchQuery(final ReviewDb db, String query,
- final int limit, final String key, final Comparator<Change> cmp)
- throws OrmException, InvalidQueryException {
- try {
- final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
- final Predicate<ChangeData> visibleToMe = builder.is_visible();
- Predicate<ChangeData> q = builder.parse(query);
- q = Predicate.and(q, //
- cmp == QUERY_PREV //
- ? builder.sortkey_after(key) //
- : builder.sortkey_before(key), //
- builder.limit(limit), //
- visibleToMe //
- );
-
- ChangeQueryRewriter rewriter = queryRewriter.get();
- Predicate<ChangeData> s = rewriter.rewrite(q);
- if (!(s instanceof ChangeDataSource)) {
- s = rewriter.rewrite(Predicate.and(builder.status_open(), q));
- }
-
- if (s instanceof ChangeDataSource) {
- ArrayList<Change> r = new ArrayList<Change>();
- HashSet<Change.Id> want = new HashSet<Change.Id>();
- for (ChangeData d : ((ChangeDataSource) s).read()) {
- if (d.hasChange()) {
- // Checking visibleToMe here should be unnecessary, the
- // query should have already performed it. But we don't
- // want to trust the query rewriter that much yet.
- //
- if (visibleToMe.match(d)) {
- r.add(d.getChange());
- }
- } else {
- want.add(d.getId());
- }
- }
-
- // Here we have to check canRead. Its impossible to
- // do that test without the change object, and it being
- // missing above means we have to compute it ourselves.
- //
- if (!want.isEmpty()) {
- for (Change c : db.changes().get(want)) {
- if (canRead(c, db)) {
- r.add(c);
- }
- }
- }
-
- Collections.sort(r, cmp);
- return new ListResultSet<Change>(r);
- } else {
- throw new InvalidQueryException("Not Supported", s.toString());
- }
- } catch (QueryParseException e) {
- throw new InvalidQueryException(e.getMessage(), query);
- }
- }
-
- public void forAccount(final Account.Id id,
- final AsyncCallback<AccountDashboardInfo> callback) {
- final Account.Id me = getAccountId();
- final Account.Id target = id != null ? id : me;
- if (target == null) {
- callback.onFailure(new NoSuchEntityException());
- return;
- }
-
- run(callback, new Action<AccountDashboardInfo>() {
- public AccountDashboardInfo run(final ReviewDb db) throws OrmException,
- Failure {
- final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
- final Account user = ac.get(target);
- if (user == null) {
- throw new Failure(new NoSuchEntityException());
- }
-
- final Set<Change.Id> stars = currentUser.get().getStarredChanges();
- final ChangeAccess changes = db.changes();
- final AccountDashboardInfo d;
-
- final Set<Change.Id> openReviews = new HashSet<Change.Id>();
- final Set<Change.Id> closedReviews = new HashSet<Change.Id>();
- for (final PatchSetApproval ca : db.patchSetApprovals().openByUser(id)) {
- openReviews.add(ca.getPatchSetId().getParentKey());
- }
- for (final PatchSetApproval ca : db.patchSetApprovals()
- .closedByUser(id)) {
- closedReviews.add(ca.getPatchSetId().getParentKey());
- }
-
- d = new AccountDashboardInfo(target);
- d.setByOwner(filter(changes.byOwnerOpen(target), stars, ac, db));
- d.setClosed(filter(changes.byOwnerClosed(target), stars, ac, db));
-
- for (final ChangeInfo c : d.getByOwner()) {
- openReviews.remove(c.getId());
- }
- d.setForReview(filter(changes.get(openReviews), stars, ac, db));
- Collections.sort(d.getForReview(), ID_COMP);
-
- for (final ChangeInfo c : d.getClosed()) {
- closedReviews.remove(c.getId());
- }
- if (!closedReviews.isEmpty()) {
- d.getClosed().addAll(filter(changes.get(closedReviews), stars, ac, db));
- Collections.sort(d.getClosed(), SORT_KEY_COMP);
- }
-
- // User dashboards are visible to other users, if the current user
- // can see any of the changes in the dashboard.
- if (!target.equals(me)
- && d.getByOwner().isEmpty()
- && d.getClosed().isEmpty()
- && d.getForReview().isEmpty()) {
- throw new Failure(new NoSuchEntityException());
- }
-
- d.setAccounts(ac.create());
- return d;
- }
- });
}
public void toggleStars(final ToggleStarRequest req,
@@ -298,97 +71,4 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
}
});
}
-
- public void myStarredChangeIds(final AsyncCallback<Set<Change.Id>> callback) {
- callback.onSuccess(currentUser.get().getStarredChanges());
- }
-
- private int safePageSize(final int pageSize) throws InvalidQueryException {
- int maxLimit = currentUser.get().getCapabilities()
- .getRange(GlobalCapability.QUERY_LIMIT)
- .getMax();
- if (maxLimit <= 0) {
- throw new InvalidQueryException("Search Disabled");
- }
- return 0 < pageSize && pageSize <= maxLimit ? pageSize : maxLimit;
- }
-
- private List<ChangeInfo> filter(final ResultSet<Change> rs,
- final Set<Change.Id> starred, final AccountInfoCacheFactory accts,
- final ReviewDb db) throws OrmException {
- final ArrayList<ChangeInfo> r = new ArrayList<ChangeInfo>();
- for (final Change c : rs) {
- if (canRead(c, db)) {
- final ChangeInfo ci = new ChangeInfo(c);
- accts.want(ci.getOwner());
- ci.setStarred(starred.contains(ci.getId()));
- r.add(ci);
- }
- }
- return r;
- }
-
- private abstract class QueryNext implements Action<SingleListChangeInfo> {
- protected final String pos;
- protected final int limit;
- protected final int slim;
-
- QueryNext(final int pageSize, final String pos) throws InvalidQueryException {
- this.pos = pos;
- this.limit = safePageSize(pageSize);
- this.slim = limit + 1;
- }
-
- public SingleListChangeInfo run(final ReviewDb db) throws OrmException,
- InvalidQueryException {
- final AccountInfoCacheFactory ac = accountInfoCacheFactory.create();
- final SingleListChangeInfo d = new SingleListChangeInfo();
- final Set<Change.Id> starred = currentUser.get().getStarredChanges();
-
- final ArrayList<ChangeInfo> list = new ArrayList<ChangeInfo>();
- final ResultSet<Change> rs = query(db, slim, pos);
- for (final Change c : rs) {
- if (!canRead(c, db)) {
- continue;
- }
- final ChangeInfo ci = new ChangeInfo(c);
- ac.want(ci.getOwner());
- ci.setStarred(starred.contains(ci.getId()));
- list.add(ci);
- if (list.size() == slim) {
- rs.close();
- break;
- }
- }
-
- final boolean atEnd = finish(list);
- d.setChanges(list, atEnd);
- d.setAccounts(ac.create());
- return d;
- }
-
- boolean finish(final ArrayList<ChangeInfo> list) {
- final boolean atEnd = list.size() <= limit;
- if (list.size() == slim) {
- list.remove(limit);
- }
- return atEnd;
- }
-
- abstract ResultSet<Change> query(final ReviewDb db, final int slim,
- String sortKey) throws OrmException, InvalidQueryException;
- }
-
- private abstract class QueryPrev extends QueryNext {
- QueryPrev(int pageSize, String pos) throws InvalidQueryException {
- super(pageSize, pos);
- }
-
- @Override
- boolean finish(final ArrayList<ChangeInfo> list) {
- final boolean atEnd = super.finish(list);
- Collections.reverse(list);
- return atEnd;
- }
- }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 3513f89c15..1b247920cb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -14,43 +14,71 @@
package com.google.gerrit.httpd.rpc;
+import com.google.common.collect.Lists;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.audit.Audit;
import com.google.gerrit.common.auth.SignInRequired;
import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.CurrentUser;
import com.google.gson.GsonBuilder;
import com.google.gwtjsonrpc.common.RemoteJsonService;
import com.google.gwtjsonrpc.server.ActiveCall;
import com.google.gwtjsonrpc.server.JsonServlet;
+import com.google.gwtjsonrpc.server.MethodHandle;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-
/**
* Base JSON servlet to ensure the current user is not forged.
*/
@SuppressWarnings("serial")
final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
+ private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class);
+ private static final ThreadLocal<GerritCall> currentCall =
+ new ThreadLocal<GerritCall>();
+ private static final ThreadLocal<MethodHandle> currentMethod =
+ new ThreadLocal<MethodHandle>();
private final Provider<WebSession> session;
private final RemoteJsonService service;
+ private final AuditService audit;
+
@Inject
- GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s) {
+ GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s,
+ final AuditService a) {
session = w;
service = s;
+ audit = a;
}
@Override
protected GerritCall createActiveCall(final HttpServletRequest req,
final HttpServletResponse rsp) {
- return new GerritCall(session.get(), req, rsp);
+ final GerritCall call = new GerritCall(session.get(), req, rsp);
+ currentCall.set(call);
+ return call;
}
@Override
protected GsonBuilder createGsonBuilder() {
- final GsonBuilder g = super.createGsonBuilder();
+ return gerritDefaultGsonBuilder();
+ }
+
+ private static GsonBuilder gerritDefaultGsonBuilder() {
+ final GsonBuilder g = defaultGsonBuilder();
g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class,
new org.eclipse.jgit.diff.EditDeserializer());
@@ -83,13 +111,117 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
return service;
}
+ @Override
+ protected void service(final HttpServletRequest req,
+ final HttpServletResponse resp) throws IOException {
+ try {
+ super.service(req, resp);
+ } finally {
+ audit();
+ currentCall.set(null);
+ }
+ }
+
+ private void audit() {
+ try {
+ GerritCall call = currentCall.get();
+ MethodHandle method = call.getMethod();
+ if (method == null) {
+ return;
+ }
+ Audit note = (Audit) method.getAnnotation(Audit.class);
+ if (note != null) {
+ final String sid = call.getWebSession().getToken();
+ final CurrentUser username = call.getWebSession().getCurrentUser();
+ final List<Object> args =
+ extractParams(note, call);
+ final String what = extractWhat(note, method.getName());
+ final Object result = call.getResult();
+
+ audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
+ result));
+ }
+ } catch (Throwable all) {
+ log.error("Unable to log the call", all);
+ }
+ }
+
+ private List<Object> extractParams(final Audit note, final GerritCall call) {
+ List<Object> args = Lists.newArrayList(Arrays.asList(call.getParams()));
+ for (int idx : note.obfuscate()) {
+ args.set(idx, "*****");
+ }
+ return args;
+ }
+
+ private String extractWhat(final Audit note, final String methodName) {
+ String what = note.action();
+ if (what.length() == 0) {
+ boolean ccase = Character.isLowerCase(methodName.charAt(0));
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < methodName.length(); i++) {
+ char c = methodName.charAt(i);
+ if (ccase && !Character.isLowerCase(c)) {
+ sb.append(' ');
+ }
+ sb.append(Character.toLowerCase(c));
+ }
+ what = sb.toString();
+ }
+
+ return what;
+ }
+
static class GerritCall extends ActiveCall {
private final WebSession session;
+ private final long when;
+ private static final Field resultField;
+
+ // Needed to allow access to non-public result field in GWT/JSON-RPC
+ static {
+ Field declaredField = null;
+ try {
+ declaredField = ActiveCall.class.getDeclaredField("result");
+ declaredField.setAccessible(true);
+ } catch (Exception e) {
+ log.error("Unable to expose RPS/JSON result field");
+ }
+
+ resultField = declaredField;
+ }
+
+ // Surrogate of the missing getResult() in GWT/JSON-RPC
+ public Object getResult() {
+ if (resultField == null) {
+ return null;
+ }
+
+ try {
+ return resultField.get(this);
+ } catch (IllegalArgumentException e) {
+ log.error("Cannot access result field");
+ } catch (IllegalAccessException e) {
+ log.error("No permissions to access result field");
+ }
+
+ return null;
+ }
GerritCall(final WebSession session, final HttpServletRequest i,
final HttpServletResponse o) {
super(i, o);
this.session = session;
+ this.when = System.currentTimeMillis();
+ }
+
+ @Override
+ public MethodHandle getMethod() {
+ if (currentMethod.get() == null) {
+ return super.getMethod();
+ } else {
+ return currentMethod.get();
+ }
}
@Override
@@ -120,5 +252,18 @@ final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall>
return session.isSignedIn() && session.isTokenValid(keyIn);
}
}
+
+ public WebSession getWebSession() {
+ return session;
+ }
+
+ public long getWhen() {
+ return when;
+ }
+
+ public long getElapsed() {
+ return System.currentTimeMillis() - when;
+ }
}
+
}
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 f3e8e65ea0..1baa49b514 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
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd.rpc;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.ReviewerInfo;
@@ -21,8 +23,6 @@ import com.google.gerrit.common.data.SuggestService;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -31,15 +31,13 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountVisibility;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.patch.AddReviewer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.server.OrmException;
@@ -55,46 +53,43 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nullable;
+
class SuggestServiceImpl extends BaseServiceImplementation implements
SuggestService {
private static final String MAX_SUFFIX = "\u9fa5";
private final Provider<ReviewDb> reviewDbProvider;
- private final ProjectControl.Factory projectControlFactory;
- private final ProjectCache projectCache;
private final AccountCache accountCache;
- private final GroupControl.Factory groupControlFactory;
private final GroupMembers.Factory groupMembersFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final AccountControl.Factory accountControlFactory;
private final ChangeControl.Factory changeControlFactory;
+ private final ProjectControl.Factory projectControlFactory;
private final Config cfg;
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final boolean suggestAccounts;
@Inject
SuggestServiceImpl(final Provider<ReviewDb> schema,
- final ProjectControl.Factory projectControlFactory,
- final ProjectCache projectCache, final AccountCache accountCache,
- final GroupControl.Factory groupControlFactory,
+ final AccountCache accountCache,
final GroupMembers.Factory groupMembersFactory,
final Provider<CurrentUser> currentUser,
final IdentifiedUser.GenericFactory identifiedUserFactory,
final AccountControl.Factory accountControlFactory,
final ChangeControl.Factory changeControlFactory,
- @GerritServerConfig final Config cfg, final GroupCache groupCache) {
+ final ProjectControl.Factory projectControlFactory,
+ @GerritServerConfig final Config cfg, final GroupBackend groupBackend) {
super(schema, currentUser);
this.reviewDbProvider = schema;
- this.projectControlFactory = projectControlFactory;
- this.projectCache = projectCache;
this.accountCache = accountCache;
- this.groupControlFactory = groupControlFactory;
this.groupMembersFactory = groupMembersFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.accountControlFactory = accountControlFactory;
this.changeControlFactory = changeControlFactory;
+ this.projectControlFactory = projectControlFactory;
this.cfg = cfg;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) {
this.suggestAccounts = false;
@@ -111,28 +106,6 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
}
}
- public void suggestProjectNameKey(final String query, final int limit,
- final AsyncCallback<List<Project.NameKey>> callback) {
- final int max = 10;
- final int n = limit <= 0 ? max : Math.min(limit, max);
-
- final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
- for (final Project.NameKey nameKey : projectCache.byName(query)) {
- final ProjectControl ctl;
- try {
- ctl = projectControlFactory.validateFor(nameKey);
- } catch (NoSuchProjectException e) {
- continue;
- }
-
- r.add(ctl.getProject().getNameKey());
- if (r.size() == n) {
- break;
- }
- }
- callback.onSuccess(r);
- }
-
private interface VisibilityControl {
boolean isVisible(Account account) throws OrmException;
}
@@ -205,33 +178,31 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
public void suggestAccountGroup(final String query, final int limit,
final AsyncCallback<List<GroupReference>> callback) {
- run(callback, new Action<List<GroupReference>>() {
- public List<GroupReference> run(final ReviewDb db) throws OrmException {
- return suggestAccountGroup(db, query, limit);
- }
- });
+ suggestAccountGroupForProject(null, query, limit, callback);
}
- private List<GroupReference> suggestAccountGroup(final ReviewDb db,
- final String query, final int limit) throws OrmException {
- final String a = query;
- final String b = a + MAX_SUFFIX;
- final int max = 10;
- final int n = limit <= 0 ? max : Math.min(limit, max);
- List<GroupReference> r = new ArrayList<GroupReference>(n);
- for (AccountGroupName group : db.accountGroupNames().suggestByName(a, b, n)) {
- try {
- if (groupControlFactory.controlFor(group.getId()).isVisible()) {
- AccountGroup g = groupCache.get(group.getId());
- if (g != null && g.getGroupUUID() != null) {
- r.add(GroupReference.forGroup(g));
+ public void suggestAccountGroupForProject(final Project.NameKey project,
+ final String query, final int limit,
+ final AsyncCallback<List<GroupReference>> callback) {
+ run(callback, new Action<List<GroupReference>>() {
+ public List<GroupReference> run(final ReviewDb db) {
+ ProjectControl projectControl = null;
+ if (project != null) {
+ try {
+ projectControl = projectControlFactory.controlFor(project);
+ } catch (NoSuchProjectException e) {
+ return Collections.emptyList();
}
}
- } catch (NoSuchGroupException e) {
- continue;
+ return suggestAccountGroup(projectControl, query, limit);
}
- }
- return r;
+ });
+ }
+
+ 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));
}
@Override
@@ -258,7 +229,9 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
public boolean isVisible(Account account) throws OrmException {
IdentifiedUser who =
identifiedUserFactory.create(reviewDbProvider, account.getId());
- return changeControl.forUser(who).isVisible(reviewDbProvider.get());
+ // we can't use changeControl directly as it won't suggest reviewers
+ // to drafts
+ return changeControl.forUser(who).isRefVisible();
}
};
@@ -270,7 +243,7 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
reviewer.add(new ReviewerInfo(a));
}
final List<GroupReference> suggestedAccountGroups =
- suggestAccountGroup(db, query, limit);
+ suggestAccountGroup(changeControl.getProjectControl(), query, limit);
for (final GroupReference g : suggestedAccountGroups) {
if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) {
reviewer.add(new ReviewerInfo(g));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 9de7d88658..931605cbf8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,16 +14,15 @@
package com.google.gerrit.httpd.rpc;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.SshHostKey;
import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -34,6 +33,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -44,32 +44,31 @@ class SystemInfoServiceImpl implements SystemInfoService {
private static final JSch JSCH = new JSch();
- private final SchemaFactory<ReviewDb> schema;
private final List<HostKey> hostKeys;
private final Provider<HttpServletRequest> httpRequest;
private final Provider<GerritConfig> config;
+ private final ProjectCache projectCache;
@Inject
- SystemInfoServiceImpl(final SchemaFactory<ReviewDb> sf, final SshInfo daemon,
- final Provider<HttpServletRequest> hsr, Provider<GerritConfig> cfg) {
- schema = sf;
+ SystemInfoServiceImpl(final SshInfo daemon,
+ final Provider<HttpServletRequest> hsr, final Provider<GerritConfig> cfg,
+ final ProjectCache pc) {
hostKeys = daemon.getHostKeys();
httpRequest = hsr;
config = cfg;
+ projectCache = pc;
}
public void contributorAgreements(
final AsyncCallback<List<ContributorAgreement>> callback) {
- try {
- final ReviewDb db = schema.open();
- try {
- callback.onSuccess(db.contributorAgreements().active().toList());
- } finally {
- db.close();
- }
- } catch (OrmException e) {
- callback.onFailure(e);
+ Collection<ContributorAgreement> agreements =
+ projectCache.getAllProjects().getConfig().getContributorAgreements();
+ List<ContributorAgreement> cas =
+ Lists.newArrayListWithCapacity(agreements.size());
+ for (ContributorAgreement ca : agreements) {
+ cas.add(ca.forUi());
}
+ callback.onSuccess(cas);
}
public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
new file mode 100644
index 0000000000..a33c2093af
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
@@ -0,0 +1,189 @@
+// 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.httpd.rpc.account;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+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.START_REPLICATION;
+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.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class AccountCapabilitiesServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private final ParameterParser paramParser;
+ private final Provider<Impl> factory;
+
+ @Inject
+ AccountCapabilitiesServlet(final Provider<CurrentUser> currentUser,
+ ParameterParser paramParser, Provider<Impl> factory) {
+ super(currentUser);
+ this.paramParser = paramParser;
+ this.factory = factory;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ Impl impl = factory.get();
+ if (acceptsJson(req)) {
+ impl.format = OutputFormat.JSON_COMPACT;
+ }
+ if (paramParser.parse(impl, req, res)) {
+ impl.compute();
+
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
+ if (impl.format.isJson()) {
+ res.setContentType(JSON_TYPE);
+ buf.write(JSON_MAGIC);
+ impl.format.newGson().toJson(
+ impl.have,
+ new TypeToken<Map<String, Object>>() {}.getType(),
+ out);
+ out.flush();
+ buf.write('\n');
+ } else {
+ res.setContentType("text/plain");
+ for (Map.Entry<String, Object> e : impl.have.entrySet()) {
+ out.write(e.getKey());
+ if (!(e.getValue() instanceof Boolean)) {
+ out.write(": ");
+ out.write(e.getValue().toString());
+ }
+ out.write('\n');
+ }
+ out.flush();
+ }
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+
+ static class Impl {
+ private final CapabilityControl cc;
+ private final Map<String, Object> have;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
+ void addQuery(String name) {
+ if (query == null) {
+ query = Sets.newHashSet();
+ }
+ query.add(name.toLowerCase());
+ }
+ private Set<String> query;
+
+ @Inject
+ Impl(CurrentUser user) {
+ cc = user.getCapabilities();
+ have = Maps.newLinkedHashMap();
+ }
+
+ void compute() {
+ for (String name : GlobalCapability.getAllNames()) {
+ if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+ if (GlobalCapability.hasRange(name)) {
+ have.put(name, new Range(cc.getRange(name)));
+ } else {
+ have.put(name, true);
+ }
+ }
+ }
+
+ have.put(CREATE_ACCOUNT, cc.canCreateAccount());
+ have.put(CREATE_GROUP, cc.canCreateGroup());
+ have.put(CREATE_PROJECT, cc.canCreateProject());
+ have.put(KILL_TASK, cc.canKillTask());
+ have.put(VIEW_CACHES, cc.canViewCaches());
+ have.put(FLUSH_CACHES, cc.canFlushCaches());
+ have.put(VIEW_CONNECTIONS, cc.canViewConnections());
+ have.put(VIEW_QUEUE, cc.canViewQueue());
+ have.put(START_REPLICATION, cc.canStartReplication());
+
+ QueueProvider.QueueType queue = cc.getQueueType();
+ if (queue != QueueProvider.QueueType.INTERACTIVE
+ || (query != null && query.contains(PRIORITY))) {
+ have.put(PRIORITY, queue);
+ }
+
+ Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<String, Object> e = itr.next();
+ if (!want(e.getKey())) {
+ itr.remove();
+ } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
+ itr.remove();
+ }
+ }
+ }
+
+ private boolean want(String name) {
+ return query == null || query.contains(name.toLowerCase());
+ }
+ }
+
+ private static class Range {
+ private transient PermissionRange range;
+ @SuppressWarnings("unused")
+ private int min;
+ @SuppressWarnings("unused")
+ private int max;
+
+ Range(PermissionRange r) {
+ range = r;
+ min = r.getMin();
+ max = r.getMax();
+ }
+
+ @Override
+ public String toString() {
+ return range.toString();
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index aa94759f30..e3b7408c0c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,23 +14,26 @@
package com.google.gerrit.httpd.rpc.account;
+import com.google.common.base.Strings;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.errors.ContactInformationStoreException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -42,12 +45,14 @@ import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.ClearPassword;
import com.google.gerrit.server.account.GeneratePassword;
+import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -68,6 +73,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final ContactStore contactStore;
private final AuthConfig authConfig;
private final Realm realm;
+ private final ProjectCache projectCache;
private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
@@ -85,12 +91,13 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
private final MyGroupsFactory.Factory myGroupsFactory;
private final ChangeHooks hooks;
+ private final GroupCache groupCache;
@Inject
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
- final EmailTokenVerifier etv,
+ final EmailTokenVerifier etv, final ProjectCache pc,
final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
@@ -100,13 +107,14 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final MyGroupsFactory.Factory myGroupsFactory,
- final ChangeHooks hooks) {
+ final ChangeHooks hooks, final GroupCache groupCache) {
super(schema, currentUser);
contactStore = cs;
authConfig = ac;
realm = r;
user = u;
emailTokenVerifier = etv;
+ projectCache = pc;
registerNewEmailFactory = esf;
sshKeyCache = skc;
byEmailCache = abec;
@@ -122,6 +130,7 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
this.externalIdDetailFactory = externalIdDetailFactory;
this.myGroupsFactory = myGroupsFactory;
this.hooks = hooks;
+ this.groupCache = groupCache;
}
public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
@@ -205,9 +214,9 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
}
@Override
- public void myGroups(final AsyncCallback<List<GroupDetail>> callback) {
- run(callback, new Action<List<GroupDetail>>() {
- public List<GroupDetail> run(final ReviewDb db) throws OrmException,
+ public void myGroups(final AsyncCallback<List<AccountGroup>> callback) {
+ run(callback, new Action<List<AccountGroup>>() {
+ public List<AccountGroup> run(final ReviewDb db) throws OrmException,
NoSuchGroupException, Failure {
return myGroupsFactory.create().call();
}
@@ -223,12 +232,17 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
final ContactInformation info, final AsyncCallback<Account> callback) {
run(callback, new Action<Account>() {
public Account run(ReviewDb db) throws OrmException, Failure {
- final Account me = db.accounts().get(user.get().getAccountId());
+ IdentifiedUser self = user.get();
+ final Account me = db.accounts().get(self.getAccountId());
final String oldEmail = me.getPreferredEmail();
if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
- me.setFullName(name != null && !name.isEmpty() ? name : null);
+ me.setFullName(Strings.emptyToNull(name));
}
- me.setPreferredEmail(emailAddr);
+ if (!Strings.isNullOrEmpty(emailAddr)
+ && !self.getEmailAddresses().contains(emailAddr)) {
+ throw new Failure(new PermissionDeniedException("Email address must be verified"));
+ }
+ me.setPreferredEmail(Strings.emptyToNull(emailAddr));
if (useContactInfo) {
if (ContactInformation.hasAddress(info)
|| (me.isContactFiled() && ContactInformation.hasData(info))) {
@@ -260,24 +274,43 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
return a != null && a.equals(b);
}
- public void enterAgreement(final ContributorAgreement.Id id,
+ public void enterAgreement(final String agreementName,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
- final ContributorAgreement cla = db.contributorAgreements().get(id);
- if (cla == null || !cla.isActive()) {
+ ContributorAgreement ca = projectCache.getAllProjects().getConfig()
+ .getContributorAgreement(agreementName);
+ if (ca == null) {
+ throw new Failure(new NoSuchEntityException());
+ }
+
+ if (ca.getAutoVerify() == null) {
+ throw new Failure(new IllegalStateException(
+ "cannot enter a non-autoVerify agreement"));
+ } else if (ca.getAutoVerify().getUUID() == null) {
throw new Failure(new NoSuchEntityException());
}
- final AccountAgreement a =
- new AccountAgreement(new AccountAgreement.Key(user.get()
- .getAccountId(), id));
- if (cla.isAutoVerify()) {
- a.review(AccountAgreement.Status.VERIFIED, null);
+ AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
+ if (group == null) {
+ throw new Failure(new NoSuchEntityException());
+ }
- hooks.doClaSignupHook(user.get().getAccount(), cla);
+ Account account = user.get().getAccount();
+ hooks.doClaSignupHook(account, ca);
+
+ final AccountGroupMember.Key key =
+ new AccountGroupMember.Key(account.getId(), group.getId());
+ AccountGroupMember m = db.accountGroupMembers().get(key);
+ if (m == null) {
+ m = new AccountGroupMember(key);
+ db.accountGroupMembersAudit().insert(
+ Collections.singleton(
+ new AccountGroupMemberAudit(m, account.getId())));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ accountCache.evict(m.getAccountId());
}
- db.accountAgreements().insert(Collections.singleton(a));
+
return VoidResult.INSTANCE;
}
});
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index 39712e4abd..712f5a227f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -14,83 +14,71 @@
package com.google.gerrit.httpd.rpc.account;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
import java.util.List;
import java.util.Map;
class AgreementInfoFactory extends Handler<AgreementInfo> {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
interface Factory {
AgreementInfoFactory create();
}
- private final ReviewDb db;
- private final GroupCache groupCache;
private final IdentifiedUser user;
+ private final ProjectCache projectCache;
private AgreementInfo info;
@Inject
- AgreementInfoFactory(final ReviewDb db, final GroupCache groupCache,
- final IdentifiedUser user) {
- this.db = db;
- this.groupCache = groupCache;
+ AgreementInfoFactory(final IdentifiedUser user,
+ final ProjectCache projectCache) {
this.user = user;
+ this.projectCache = projectCache;
}
@Override
public AgreementInfo call() throws Exception {
- final List<AccountAgreement> userAccepted =
- db.accountAgreements().byAccount(user.getAccountId()).toList();
-
- Collections.reverse(userAccepted);
-
- final List<AccountGroupAgreement> groupAccepted =
- new ArrayList<AccountGroupAgreement>();
- for (final AccountGroup.UUID groupUUID : user.getEffectiveGroups().getKnownGroups()) {
- AccountGroup group = groupCache.get(groupUUID);
- if (group == null) {
- continue;
- }
+ List<String> accepted = Lists.newArrayList();
+ Map<String, ContributorAgreement> agreements = Maps.newHashMap();
+ Collection<ContributorAgreement> cas =
+ projectCache.getAllProjects().getConfig().getContributorAgreements();
+ for (ContributorAgreement ca : cas) {
+ agreements.put(ca.getName(), ca.forUi());
- final List<AccountGroupAgreement> temp =
- db.accountGroupAgreements().byGroup(group.getId()).toList();
-
- Collections.reverse(temp);
-
- groupAccepted.addAll(temp);
- }
-
- final Map<ContributorAgreement.Id, ContributorAgreement> agreements =
- new HashMap<ContributorAgreement.Id, ContributorAgreement>();
- for (final AccountAgreement a : userAccepted) {
- final ContributorAgreement.Id id = a.getAgreementId();
- if (!agreements.containsKey(id)) {
- agreements.put(id, db.contributorAgreements().get(id));
+ List<AccountGroup.UUID> groupIds = Lists.newArrayList();
+ for (PermissionRule rule : ca.getAccepted()) {
+ if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
+ if (rule.getGroup().getUUID() == null) {
+ log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
+ " exist, referenced in CLA \"" + ca.getName() + "\"");
+ } else {
+ groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+ }
+ }
}
- }
- for (final AccountGroupAgreement a : groupAccepted) {
- final ContributorAgreement.Id id = a.getAgreementId();
- if (!agreements.containsKey(id)) {
- agreements.put(id, db.contributorAgreements().get(id));
+ if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
+ accepted.add(ca.getName());
}
}
info = new AgreementInfo();
- info.setUserAccepted(userAccepted);
- info.setGroupAccepted(groupAccepted);
+ info.setAccepted(accepted);
info.setAgreements(agreements);
return info;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index e90c2bffda..aca2e055dc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -18,6 +18,7 @@ import com.google.gerrit.common.data.GroupAdminService;
import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.data.GroupList;
import com.google.gerrit.common.data.GroupOptions;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.InactiveAccountException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchAccountException;
@@ -30,33 +31,38 @@ import com.google.gerrit.reviewdb.client.AccountGroupInclude;
import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupIncludeCache;
-import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
class GroupAdminServiceImpl extends BaseServiceImplementation implements
GroupAdminService {
private final AccountCache accountCache;
private final AccountResolver accountResolver;
- private final Realm accountRealm;
+ private final AccountManager accountManager;
+ private final AuthType authType;
private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final GroupIncludeCache groupIncludeCache;
private final GroupControl.Factory groupControlFactory;
@@ -70,8 +76,11 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
final Provider<IdentifiedUser> currentUser,
final AccountCache accountCache,
final GroupIncludeCache groupIncludeCache,
- final AccountResolver accountResolver, final Realm accountRealm,
+ final AccountResolver accountResolver,
+ final AccountManager accountManager,
+ final AuthConfig authConfig,
final GroupCache groupCache,
+ final GroupBackend groupBackend,
final GroupControl.Factory groupControlFactory,
final CreateGroup.Factory createGroupFactory,
final RenameGroup.Factory renameGroupFactory,
@@ -81,8 +90,10 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
this.accountCache = accountCache;
this.groupIncludeCache = groupIncludeCache;
this.accountResolver = accountResolver;
- this.accountRealm = accountRealm;
+ this.accountManager = accountManager;
+ this.authType = authConfig.getAuthType();
this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.groupControlFactory = groupControlFactory;
this.createGroupFactory = createGroupFactory;
this.renameGroupFactory = renameGroupFactory;
@@ -145,13 +156,13 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
final AccountGroup group = db.accountGroups().get(groupId);
assertAmGroupOwner(db, group);
- final AccountGroup owner =
- groupCache.get(new AccountGroup.NameKey(newOwnerName));
+ GroupReference owner =
+ GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
if (owner == null) {
throw new Failure(new NoSuchEntityException());
}
- group.setOwnerGroupId(owner.getId());
+ group.setOwnerGroupUUID(owner.getUUID());
db.accountGroups().update(Collections.singleton(group));
groupCache.evict(group);
return VoidResult.INSTANCE;
@@ -178,43 +189,13 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
});
}
- public void changeExternalGroup(final AccountGroup.Id groupId,
- final AccountGroup.ExternalNameKey bindTo,
- final AsyncCallback<VoidResult> callback) {
- run(callback, new Action<VoidResult>() {
- public VoidResult run(final ReviewDb db) throws OrmException, Failure {
- final AccountGroup group = db.accountGroups().get(groupId);
- assertAmGroupOwner(db, group);
- group.setExternalNameKey(bindTo);
- db.accountGroups().update(Collections.singleton(group));
- groupCache.evict(group);
- return VoidResult.INSTANCE;
- }
- });
- }
-
- public void searchExternalGroups(final String searchFilter,
- final AsyncCallback<List<AccountGroup.ExternalNameKey>> callback) {
- final ArrayList<AccountGroup.ExternalNameKey> matches =
- new ArrayList<AccountGroup.ExternalNameKey>(
- accountRealm.lookupGroups(searchFilter));
- Collections.sort(matches, new Comparator<AccountGroup.ExternalNameKey>() {
- @Override
- public int compare(AccountGroup.ExternalNameKey a,
- AccountGroup.ExternalNameKey b) {
- return a.get().compareTo(b.get());
- }
- });
- callback.onSuccess(matches);
- }
-
public void addGroupMember(final AccountGroup.Id groupId,
final String nameOrEmail, final AsyncCallback<GroupDetail> callback) {
run(callback, new Action<GroupDetail>() {
public GroupDetail run(ReviewDb db) throws OrmException, Failure,
NoSuchGroupException {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -249,7 +230,7 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
public GroupDetail run(ReviewDb db) throws OrmException, Failure,
NoSuchGroupException {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -282,7 +263,7 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
public VoidResult run(final ReviewDb db) throws OrmException,
NoSuchGroupException, Failure {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -336,7 +317,7 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
public VoidResult run(final ReviewDb db) throws OrmException,
NoSuchGroupException, Failure {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -396,13 +377,38 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements
private Account findAccount(final String nameOrEmail) throws OrmException,
Failure {
- final Account r = accountResolver.find(nameOrEmail);
+ Account r = accountResolver.find(nameOrEmail);
if (r == null) {
- throw new Failure(new NoSuchAccountException(nameOrEmail));
+ switch (authType) {
+ case HTTP_LDAP:
+ case CLIENT_SSL_CERT_LDAP:
+ case LDAP:
+ r = createAccountByLdap(nameOrEmail);
+ break;
+ default:
+ }
+ if (r == null) {
+ throw new Failure(new NoSuchAccountException(nameOrEmail));
+ }
}
return r;
}
+ private Account createAccountByLdap(String user) {
+ if (!user.matches(Account.USER_NAME_PATTERN)) {
+ return null;
+ }
+
+ try {
+ final AuthRequest req = AuthRequest.forUser(user);
+ req.setSkipAuthentication(true);
+ return accountCache.get(accountManager.authenticate(req).getAccountId())
+ .getAccount();
+ } catch (AccountException e) {
+ return null;
+ }
+ }
+
private AccountGroup findGroup(final String name) throws OrmException,
Failure {
final AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
index fd274fd2d3..33ce371083 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
@@ -14,9 +14,9 @@
package com.google.gerrit.httpd.rpc.account;
-import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.VisibleGroups;
import com.google.gwtorm.server.OrmException;
@@ -24,7 +24,7 @@ import com.google.inject.Inject;
import java.util.List;
-class MyGroupsFactory extends Handler<List<GroupDetail>> {
+class MyGroupsFactory extends Handler<List<AccountGroup>> {
interface Factory {
MyGroupsFactory create();
}
@@ -39,7 +39,7 @@ class MyGroupsFactory extends Handler<List<GroupDetail>> {
}
@Override
- public List<GroupDetail> call() throws OrmException, NoSuchGroupException {
+ public List<AccountGroup> call() throws OrmException, NoSuchGroupException {
final VisibleGroups visibleGroups = visibleGroupsFactory.create();
return visibleGroups.get(user).getGroups();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index 09dc582bf2..24faf9e888 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -15,6 +15,7 @@
package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
@@ -44,7 +45,7 @@ class RenameGroup extends Handler<GroupDetail> {
@Override
public GroupDetail call() throws OrmException, NameAlreadyUsedException,
- NoSuchGroupException {
+ NoSuchGroupException, InvalidNameException {
return performRenameGroupFactory.create().renameGroup(groupId, newName);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
index 9c4c598e4e..cf443e70b3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.httpd;
+package com.google.gerrit.httpd.rpc.change;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
@@ -29,12 +29,12 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
-public class ChangeQueryServlet extends HttpServlet {
+public class DeprecatedChangeQueryServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Provider<QueryProcessor> processor;
@Inject
- ChangeQueryServlet(Provider<QueryProcessor> processor) {
+ DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
this.processor = processor;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
new file mode 100644
index 0000000000..b501d43895
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
@@ -0,0 +1,86 @@
+// 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.httpd.rpc.change;
+
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ListChanges;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListChangesServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
+ private final ParameterParser paramParser;
+ private final Provider<ListChanges> factory;
+
+ @Inject
+ ListChangesServlet(final Provider<CurrentUser> currentUser,
+ ParameterParser paramParser, Provider<ListChanges> ls) {
+ super(currentUser);
+ this.paramParser = paramParser;
+ this.factory = ls;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ ListChanges impl = factory.get();
+ if (acceptsJson(req)) {
+ impl.setFormat(OutputFormat.JSON_COMPACT);
+ }
+ if (paramParser.parse(impl, req, res)) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ if (impl.getFormat().isJson()) {
+ buf.write(JSON_MAGIC);
+ }
+
+ Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
+ try {
+ impl.query(out);
+ } catch (QueryParseException e) {
+ res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ sendText(req, res, e.getMessage());
+ return;
+ } catch (OrmException e) {
+ log.error("Error querying /changes/", e);
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return;
+ }
+ out.flush();
+
+ res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
index 3aecb0c839..2110885461 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -26,8 +26,13 @@ import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
import javax.annotation.Nullable;
class AbandonChangeHandler extends Handler<ChangeDetail> {
@@ -36,7 +41,7 @@ class AbandonChangeHandler extends Handler<ChangeDetail> {
AbandonChangeHandler create(PatchSet.Id patchSetId, String message);
}
- private final AbandonChange.Factory abandonChangeFactory;
+ private final Provider<AbandonChange> abandonChangeProvider;
private final ChangeDetailFactory.Factory changeDetailFactory;
private final PatchSet.Id patchSetId;
@@ -44,11 +49,11 @@ class AbandonChangeHandler extends Handler<ChangeDetail> {
private final String message;
@Inject
- AbandonChangeHandler(final AbandonChange.Factory abandonChangeFactory,
+ AbandonChangeHandler(final Provider<AbandonChange> abandonChangeProvider,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted final PatchSet.Id patchSetId,
@Assisted @Nullable final String message) {
- this.abandonChangeFactory = abandonChangeFactory;
+ this.abandonChangeProvider = abandonChangeProvider;
this.changeDetailFactory = changeDetailFactory;
this.patchSetId = patchSetId;
@@ -58,9 +63,12 @@ class AbandonChangeHandler extends Handler<ChangeDetail> {
@Override
public ChangeDetail call() throws NoSuchChangeException, OrmException,
EmailException, NoSuchEntityException, InvalidChangeOperationException,
- PatchSetInfoNotAvailableException {
- final ReviewResult result =
- abandonChangeFactory.create(patchSetId, message).call();
+ PatchSetInfoNotAvailableException, RepositoryNotFoundException,
+ IOException {
+ final AbandonChange abandonChange = abandonChangeProvider.get();
+ abandonChange.setChangeId(patchSetId.getParentKey());
+ abandonChange.setMessage(message);
+ final ReviewResult result = abandonChange.call();
if (result.getErrors().size() > 0) {
throw new NoSuchChangeException(result.getChangeId());
}
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 47a9395e02..6660a3dc8b 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
@@ -32,13 +32,17 @@ import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.workflow.CategoryFunction;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtorm.server.OrmException;
@@ -46,8 +50,10 @@ import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -70,6 +76,7 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
private final AccountInfoCacheFactory aic;
private final AnonymousUser anonymousUser;
private final ReviewDb db;
+ private final GitRepositoryManager repoManager;
private final Change.Id changeId;
@@ -84,6 +91,7 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
ChangeDetailFactory(final ApprovalTypes approvalTypes,
final FunctionState.Factory functionState,
final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
+ final GitRepositoryManager repoManager,
final ChangeControl.Factory changeControlFactory,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final AnonymousUser anonymousUser,
@@ -94,6 +102,7 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
this.functionState = functionState;
this.patchSetDetail = patchSetDetail;
this.db = db;
+ this.repoManager = repoManager;
this.changeControlFactory = changeControlFactory;
this.anonymousUser = anonymousUser;
this.aic = accountInfoCacheFactory.create();
@@ -106,7 +115,8 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
@Override
public ChangeDetail call() throws OrmException, NoSuchEntityException,
- PatchSetInfoNotAvailableException, NoSuchChangeException {
+ PatchSetInfoNotAvailableException, NoSuchChangeException,
+ RepositoryNotFoundException, IOException {
control = changeControlFactory.validateFor(changeId);
final Change change = control.getChange();
final PatchSet patch = db.patchSets().get(change.currentPatchSetId());
@@ -122,7 +132,9 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail.setCanAbandon(change.getStatus() != Change.Status.DRAFT && change.getStatus().isOpen() && control.canAbandon());
detail.setCanPublish(control.canPublish(db));
- detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
+ detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED
+ && control.canRestore()
+ && ProjectUtil.branchExists(repoManager, change.getDest()));
detail.setCanDeleteDraft(control.canDeleteDraft(db));
detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
changeId));
@@ -133,20 +145,21 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail.setCanEdit(control.getRefControl().canWrite());
- if (detail.getChange().getStatus().isOpen()) {
- List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
- for (SubmitRecord rec : submitRecords) {
- if (rec.labels != null) {
- for (SubmitRecord.Label lbl : rec.labels) {
- aic.want(lbl.appliedBy);
- }
- }
- if (rec.status == SubmitRecord.Status.OK && control.getRefControl().canSubmit()) {
- detail.setCanSubmit(true);
+ List<SubmitRecord> submitRecords = control.getSubmitRecords(db, patch);
+ for (SubmitRecord rec : submitRecords) {
+ if (rec.labels != null) {
+ for (SubmitRecord.Label lbl : rec.labels) {
+ aic.want(lbl.appliedBy);
}
}
- detail.setSubmitRecords(submitRecords);
+ if (detail.getChange().getStatus().isOpen()
+ && rec.status == SubmitRecord.Status.OK
+ && control.getRefControl().canSubmit()
+ && ProjectUtil.branchExists(repoManager, change.getDest())) {
+ detail.setCanSubmit(true);
+ }
}
+ detail.setSubmitRecords(submitRecords);
patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
loadPatchSets();
@@ -193,7 +206,9 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
}
private void load() throws OrmException, NoSuchChangeException {
- if (detail.getChange().getStatus().equals(Change.Status.NEW) && testMerge) {
+ final Change.Status status = detail.getChange().getStatus();
+ if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
+ testMerge) {
ChangeUtil.testMerge(opFactory, detail.getChange());
}
@@ -239,6 +254,15 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
detail.setApprovals(ad.values());
}
+ private boolean isReviewer(Change change) {
+ // Return true if the currently logged in user is a reviewer of the change.
+ try {
+ return control.isReviewer(db, new ChangeData(change));
+ } catch (OrmException e) {
+ return false;
+ }
+ }
+
private void loadCurrentPatchSet() throws OrmException,
NoSuchEntityException, PatchSetInfoNotAvailableException,
NoSuchChangeException {
@@ -264,32 +288,48 @@ public class ChangeDetailFactory extends Handler<ChangeDetail> {
}
}
- final RevId cprev = loader.patchSet.getRevision();
- final Set<Change.Id> descendants = new HashSet<Change.Id>();
- if (cprev != null) {
- for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
- final Change.Id ck = a.getPatchSet().getParentKey();
- if (descendants.add(ck)) {
- changesToGet.add(a.getPatchSet().getParentKey());
+ final Set<PatchSet.Id> descendants = new HashSet<PatchSet.Id>();
+ RevId cprev;
+ for (PatchSet p : detail.getPatchSets()) {
+ cprev = p.getRevision();
+ if (cprev != null) {
+ for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
+ if (descendants.add(a.getPatchSet())) {
+ changesToGet.add(a.getPatchSet().getParentKey());
+ }
}
}
}
final Map<Change.Id, Change> m =
db.changes().toMap(db.changes().get(changesToGet));
+ final CurrentUser currentUser = control.getCurrentUser();
+ Account.Id currentUserId = null;
+ if (currentUser instanceof IdentifiedUser) {
+ currentUserId = ((IdentifiedUser) currentUser).getAccountId();
+ }
+
final ArrayList<ChangeInfo> dependsOn = new ArrayList<ChangeInfo>();
for (final Change.Id a : ancestorOrder) {
final Change ac = m.get(a);
- if (ac != null) {
- dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
+ if (ac != null && ac.getProject().equals(detail.getChange().getProject())) {
+ if (ac.getStatus().getCode() != Change.STATUS_DRAFT
+ || ac.getOwner().equals(currentUserId)
+ || isReviewer(ac)) {
+ dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
+ }
}
}
final ArrayList<ChangeInfo> neededBy = new ArrayList<ChangeInfo>();
- for (final Change.Id a : descendants) {
- final Change ac = m.get(a);
- if (ac != null) {
- neededBy.add(newChangeInfo(ac, null));
+ for (final PatchSet.Id a : descendants) {
+ final Change ac = m.get(a.getParentKey());
+ if (ac != null && ac.currentPatchSetId().equals(a)) {
+ if (ac.getStatus().getCode() != Change.STATUS_DRAFT
+ || ac.getOwner().equals(currentUserId)
+ || isReviewer(ac)) {
+ neededBy.add(newChangeInfo(ac, null));
+ }
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 66c11c0e2a..ecd8b549c6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -19,8 +19,8 @@ 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.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -38,7 +38,7 @@ class DeleteDraftChange extends Handler<VoidResult> {
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@@ -46,7 +46,7 @@ class DeleteDraftChange extends Handler<VoidResult> {
DeleteDraftChange(final ReviewDb db,
final ChangeControl.Factory changeControlFactory,
final GitRepositoryManager gitManager,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.db = db;
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 de3ff2f065..95a8e264b8 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
@@ -31,6 +31,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -108,18 +109,19 @@ class PatchSetDetailFactory extends Handler<PatchSetDetail> {
final PatchList list;
- if (psIdBase != null) {
- oldId = toObjectId(psIdBase);
- newId = toObjectId(psIdNew);
+ try {
+ if (psIdBase != null) {
+ oldId = toObjectId(psIdBase);
+ newId = toObjectId(psIdNew);
- projectKey = control.getProject().getNameKey();
+ projectKey = control.getProject().getNameKey();
- list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
- } else { // OK, means use base to compare
- list = patchListCache.get(control.getChange(), patchSet);
- if (list == null) {
- throw new NoSuchEntityException();
+ list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+ } else { // OK, means use base to compare
+ list = patchListCache.get(control.getChange(), patchSet);
}
+ } catch (PatchListNotAvailableException e) {
+ throw new NoSuchEntityException();
}
final List<Patch> patches = list.toPatchList(patchSet.getId());
@@ -185,7 +187,8 @@ class PatchSetDetailFactory extends Handler<PatchSetDetail> {
return new PatchListKey(projectKey, oldId, newId, whitespace);
}
- private PatchList listFor(final PatchListKey key) {
+ private PatchList listFor(PatchListKey key)
+ throws PatchListNotAvailableException {
return patchListCache.get(key);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 638bfe3f71..50baf970da 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -14,12 +14,14 @@
package com.google.gerrit.httpd.rpc.changedetail;
+import com.google.gerrit.common.data.ApprovalDetail;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.PatchSetPublishDetail;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -32,6 +34,8 @@ import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.workflow.CategoryFunction;
+import com.google.gerrit.server.workflow.FunctionState;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -49,6 +53,7 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
private final PatchSetInfoFactory infoFactory;
private final ReviewDb db;
+ private final FunctionState.Factory functionState;
private final ChangeControl.Factory changeControlFactory;
private final ApprovalTypes approvalTypes;
private final AccountInfoCacheFactory aic;
@@ -64,11 +69,13 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
final ReviewDb db,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
+ final FunctionState.Factory functionState,
final ChangeControl.Factory changeControlFactory,
final ApprovalTypes approvalTypes,
final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
this.infoFactory = infoFactory;
this.db = db;
+ this.functionState = functionState;
this.changeControlFactory = changeControlFactory;
this.approvalTypes = approvalTypes;
this.aic = accountInfoCacheFactory.create();
@@ -83,7 +90,8 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl control = changeControlFactory.validateFor(changeId);
change = control.getChange();
- patchSetInfo = infoFactory.get(db, patchSetId);
+ PatchSet patchSet = db.patchSets().get(patchSetId);
+ patchSetInfo = infoFactory.get(change, patchSet);
drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
aic.want(change.getOwner());
@@ -119,7 +127,7 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
.toList();
boolean couldSubmit = false;
- List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+ List<SubmitRecord> submitRecords = control.canSubmit(db, patchSet);
for (SubmitRecord rec : submitRecords) {
if (rec.status == SubmitRecord.Status.OK) {
couldSubmit = true;
@@ -129,6 +137,8 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
int ok = 0;
for (SubmitRecord.Label lbl : rec.labels) {
+ aic.want(lbl.appliedBy);
+
boolean canMakeOk = false;
PermissionRange range = rangeByName.get(lbl.label);
if (range != null) {
@@ -144,6 +154,7 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
switch (lbl.status) {
case OK:
+ case MAY:
ok++;
break;
@@ -165,12 +176,60 @@ final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail>
if (couldSubmit && control.getRefControl().canSubmit()) {
detail.setCanSubmit(true);
}
+
+ detail.setSubmitRecords(submitRecords);
}
detail.setLabels(allowed);
detail.setGiven(given);
+ loadApprovals(detail, control);
+
detail.setAccounts(aic.create());
return detail;
}
+
+ private void loadApprovals(final PatchSetPublishDetail detail,
+ final ChangeControl control) throws OrmException {
+ final PatchSet.Id psId = detail.getChange().currentPatchSetId();
+ final Change.Id changeId = patchSetId.getParentKey();
+ final List<PatchSetApproval> allApprovals =
+ db.patchSetApprovals().byChange(changeId).toList();
+
+ if (detail.getChange().getStatus().isOpen()) {
+ final FunctionState fs = functionState.create(control, psId, allApprovals);
+
+ for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
+ CategoryFunction.forCategory(at.getCategory()).run(at, fs);
+ }
+ }
+
+ final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
+ && control.getCurrentUser() instanceof IdentifiedUser;
+ final HashMap<Account.Id, ApprovalDetail> ad =
+ new HashMap<Account.Id, ApprovalDetail>();
+ for (PatchSetApproval ca : allApprovals) {
+ ApprovalDetail d = ad.get(ca.getAccountId());
+ if (d == null) {
+ d = new ApprovalDetail(ca.getAccountId());
+ d.setCanRemove(canRemoveReviewers);
+ ad.put(d.getAccount(), d);
+ }
+ if (d.canRemove()) {
+ d.setCanRemove(control.canRemoveReviewer(ca));
+ }
+ if (ca.getPatchSetId().equals(psId)) {
+ d.add(ca);
+ }
+ }
+
+ final Account.Id owner = detail.getChange().getOwner();
+ if (ad.containsKey(owner)) {
+ // Ensure the owner always sorts to the top of the table
+ ad.get(owner).sortFirst();
+ }
+
+ aic.want(ad.keySet());
+ detail.setApprovals(ad.values());
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
index 4ea279fbc1..f57b29c449 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
@@ -26,6 +26,10 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
class PublishAction extends Handler<ChangeDetail> {
interface Factory {
PublishAction create(PatchSet.Id patchSetId);
@@ -49,7 +53,7 @@ class PublishAction extends Handler<ChangeDetail> {
@Override
public ChangeDetail call() throws OrmException, NoSuchEntityException,
IllegalStateException, PatchSetInfoNotAvailableException,
- NoSuchChangeException {
+ NoSuchChangeException, RepositoryNotFoundException, IOException {
final ReviewResult result = publishFactory.create(patchSetId).call();
if (result.getErrors().size() > 0) {
throw new IllegalStateException("Cannot publish patchset");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
index 3c29074704..e71e302913 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -15,17 +15,17 @@
package com.google.gerrit.httpd.rpc.changedetail;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.Handler;
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.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RebasedPatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@ class RebaseChange extends Handler<ChangeDetail> {
private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@@ -65,7 +65,7 @@ class RebaseChange extends Handler<ChangeDetail> {
private final PersonIdent myIdent;
- private final ApprovalTypes approvalTypes;
+ private final ApprovalsUtil approvalsUtil;
@Inject
RebaseChange(final ChangeControl.Factory changeControlFactory,
@@ -75,9 +75,9 @@ class RebaseChange extends Handler<ChangeDetail> {
@Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
final GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@GerritPersonIdent final PersonIdent myIdent,
- final ApprovalTypes approvalTypes) {
+ final ApprovalsUtil approvalsUtil) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
@@ -92,7 +92,7 @@ class RebaseChange extends Handler<ChangeDetail> {
this.replication = replication;
this.myIdent = myIdent;
- this.approvalTypes = approvalTypes;
+ this.approvalsUtil = approvalsUtil;
}
@Override
@@ -103,7 +103,7 @@ class RebaseChange extends Handler<ChangeDetail> {
ChangeUtil.rebaseChange(patchSetId, currentUser, db,
rebasedPatchSetSenderFactory, hooks, gitManager, patchSetInfoFactory,
- replication, myIdent, changeControlFactory, approvalTypes);
+ replication, myIdent, changeControlFactory, approvalsUtil);
return changeDetailFactory.create(patchSetId.getParentKey()).call();
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
index f0187501e8..e4571fd22e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -26,8 +26,13 @@ import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
import javax.annotation.Nullable;
class RestoreChangeHandler extends Handler<ChangeDetail> {
@@ -35,7 +40,7 @@ class RestoreChangeHandler extends Handler<ChangeDetail> {
RestoreChangeHandler create(PatchSet.Id patchSetId, String message);
}
- private final RestoreChange.Factory restoreChangeFactory;
+ private final Provider<RestoreChange> restoreChangeProvider;
private final ChangeDetailFactory.Factory changeDetailFactory;
private final PatchSet.Id patchSetId;
@@ -43,11 +48,11 @@ class RestoreChangeHandler extends Handler<ChangeDetail> {
private final String message;
@Inject
- RestoreChangeHandler(final RestoreChange.Factory restoreChangeFactory,
+ RestoreChangeHandler(final Provider<RestoreChange> restoreChangeProvider,
final ChangeDetailFactory.Factory changeDetailFactory,
@Assisted final PatchSet.Id patchSetId,
@Assisted @Nullable final String message) {
- this.restoreChangeFactory = restoreChangeFactory;
+ this.restoreChangeProvider = restoreChangeProvider;
this.changeDetailFactory = changeDetailFactory;
this.patchSetId = patchSetId;
@@ -57,9 +62,12 @@ class RestoreChangeHandler extends Handler<ChangeDetail> {
@Override
public ChangeDetail call() throws NoSuchChangeException, OrmException,
EmailException, NoSuchEntityException, InvalidChangeOperationException,
- PatchSetInfoNotAvailableException {
- final ReviewResult result =
- restoreChangeFactory.create(patchSetId, message).call();
+ PatchSetInfoNotAvailableException, RepositoryNotFoundException,
+ IOException {
+ final RestoreChange restoreChange = restoreChangeProvider.get();
+ restoreChange.setChangeId(patchSetId.getParentKey());
+ restoreChange.setMessage(message);
+ final ReviewResult result = restoreChange.call();
if (result.getErrors().size() > 0) {
throw new NoSuchChangeException(result.getChangeId());
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
index 9fb9ae1d78..60a03bddd6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
@@ -24,8 +24,8 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@ class RevertChange extends Handler<ChangeDetail> {
private final IdentifiedUser currentUser;
private final RevertedSender.Factory revertedSenderFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@Nullable
@@ -76,7 +76,7 @@ class RevertChange extends Handler<ChangeDetail> {
@Assisted @Nullable final String message, final ChangeHooks hooks,
final GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@GerritPersonIdent final PersonIdent myIdent) {
this.changeControlFactory = changeControlFactory;
this.db = db;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index 80100ada27..23b21d5daf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -27,6 +27,10 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
class SubmitAction extends Handler<ChangeDetail> {
interface Factory {
SubmitAction create(PatchSet.Id patchSetId);
@@ -50,7 +54,8 @@ class SubmitAction extends Handler<ChangeDetail> {
@Override
public ChangeDetail call() throws OrmException, NoSuchEntityException,
IllegalStateException, InvalidChangeOperationException,
- PatchSetInfoNotAvailableException, NoSuchChangeException {
+ PatchSetInfoNotAvailableException, NoSuchChangeException,
+ RepositoryNotFoundException, IOException {
final ReviewResult result =
submitFactory.create(patchSetId).call();
if (result.getErrors().size() > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index e90467e826..40c9b843ab 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -52,6 +52,9 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -166,6 +169,10 @@ class PatchDetailServiceImpl extends BaseServiceImplementation implements
throw new Failure(e);
} catch (PatchSetInfoNotAvailableException e) {
throw new Failure(e);
+ } catch (RepositoryNotFoundException e) {
+ throw new Failure(e);
+ } catch (IOException e) {
+ throw new Failure(e);
}
}
});
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index ad7746f619..816bbef6eb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -147,7 +147,8 @@ class PatchScriptBuilder {
} else if (diffPrefs.isIntralineDifference()) {
IntraLineDiff d =
patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
- b.id, b.src, edits, projectKey, bId, b.path));
+ b.id, b.src, edits, projectKey, bId, b.path,
+ diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
if (d != null) {
switch (d.getStatus()) {
case EDIT_LIST:
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index d61e6e755f..97d850a34d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -36,6 +36,7 @@ import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
@@ -143,6 +144,9 @@ class PatchScriptFactory extends Handler<PatchScript> {
} catch (RepositoryNotFoundException e) {
log.error("Repository " + projectKey + " not found", e);
throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("Cannot open repository " + projectKey, e);
+ throw new NoSuchChangeException(changeId, e);
}
try {
final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
@@ -153,14 +157,14 @@ class PatchScriptFactory extends Handler<PatchScript> {
content.getOldName(), //
content.getNewName());
- try {
return b.toPatchScript(content, comments, history);
- } catch (IOException e) {
- log.error("File content unavailable", e);
- throw new NoSuchChangeException(changeId, e);
- } catch (org.eclipse.jgit.errors.LargeObjectException err) {
- throw new LargeObjectException("File content is too large", err);
- }
+ } catch (PatchListNotAvailableException e) {
+ throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("File content unavailable", e);
+ throw new NoSuchChangeException(changeId, e);
+ } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+ throw new LargeObjectException("File content is too large", err);
} finally {
git.close();
}
@@ -170,7 +174,8 @@ class PatchScriptFactory extends Handler<PatchScript> {
return new PatchListKey(projectKey, aId, bId, whitespace);
}
- private PatchList listFor(final PatchListKey key) {
+ private PatchList listFor(final PatchListKey key)
+ throws PatchListNotAvailableException {
return patchListCache.get(key);
}
@@ -251,7 +256,7 @@ class PatchScriptFactory extends Handler<PatchScript> {
break;
case DELETED:
- loadPublished(byKey, aic, oldName);
+ loadPublished(byKey, aic, newName);
break;
case COPIED:
@@ -273,7 +278,7 @@ class PatchScriptFactory extends Handler<PatchScript> {
break;
case DELETED:
- loadDrafts(byKey, aic, me, oldName);
+ loadDrafts(byKey, aic, me, newName);
break;
case COPIED:
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
new file mode 100644
index 0000000000..5e8145cb5f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
@@ -0,0 +1,68 @@
+// 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.httpd.rpc.plugin;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class ListPluginsServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private final ParameterParser paramParser;
+ private final Provider<ListPlugins> factory;
+
+ @Inject
+ ListPluginsServlet(final Provider<CurrentUser> currentUser,
+ ParameterParser paramParser, Provider<ListPlugins> ls) {
+ super(currentUser);
+ this.paramParser = paramParser;
+ this.factory = ls;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ ListPlugins impl = factory.get();
+ if (acceptsJson(req)) {
+ impl.setFormat(OutputFormat.JSON_COMPACT);
+ }
+ if (paramParser.parse(impl, req, res)) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ if (impl.getFormat().isJson()) {
+ res.setContentType(JSON_TYPE);
+ buf.write(JSON_MAGIC);
+ } else {
+ res.setContentType("text/plain");
+ }
+ impl.display(buf);
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 44c3141046..97e9bb4d22 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -22,8 +22,8 @@ import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
@@ -59,7 +59,7 @@ class AddBranch extends Handler<ListBranchesResult> {
private final ListBranches.Factory listBranchesFactory;
private final IdentifiedUser identifiedUser;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated referenceUpdated;
private final ChangeHooks hooks;
private final Project.NameKey projectName;
@@ -71,7 +71,7 @@ class AddBranch extends Handler<ListBranchesResult> {
final ListBranches.Factory listBranchesFactory,
final IdentifiedUser identifiedUser,
final GitRepositoryManager repoManager,
- final ReplicationQueue replication,
+ GitReferenceUpdated referenceUpdated,
final ChangeHooks hooks,
@Assisted Project.NameKey projectName,
@@ -81,7 +81,7 @@ class AddBranch extends Handler<ListBranchesResult> {
this.listBranchesFactory = listBranchesFactory;
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
- this.replication = replication;
+ this.referenceUpdated = referenceUpdated;
this.hooks = hooks;
this.projectName = projectName;
@@ -144,7 +144,7 @@ class AddBranch extends Handler<ListBranchesResult> {
case FAST_FORWARD:
case NEW:
case NO_CHANGE:
- replication.scheduleUpdate(name.getParentKey(), refname);
+ referenceUpdated.fire(name.getParentKey(), refname);
hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
break;
default: {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 777329bd55..72b5e3a54a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -15,41 +15,26 @@
package com.google.gerrit.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.ProjectAccess;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import javax.annotation.Nullable;
-class ChangeProjectAccess extends Handler<ProjectAccess> {
+class ChangeProjectAccess extends ProjectAccessHandler<ProjectAccess> {
interface Factory {
ChangeProjectAccess create(@Assisted Project.NameKey projectName,
@Nullable @Assisted ObjectId base,
@@ -58,152 +43,29 @@ class ChangeProjectAccess extends Handler<ProjectAccess> {
}
private final ProjectAccessFactory.Factory projectAccessFactory;
- private final ProjectControl.Factory projectControlFactory;
private final ProjectCache projectCache;
- private final GroupCache groupCache;
- private final MetaDataUpdate.User metaDataUpdateFactory;
-
- private final Project.NameKey projectName;
- private final ObjectId base;
- private List<AccessSection> sectionList;
- private String message;
@Inject
ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
final ProjectControl.Factory projectControlFactory,
- final ProjectCache projectCache, final GroupCache groupCache,
+ final ProjectCache projectCache, final GroupBackend groupBackend,
final MetaDataUpdate.User metaDataUpdateFactory,
@Assisted final Project.NameKey projectName,
@Nullable @Assisted final ObjectId base,
@Assisted List<AccessSection> sectionList,
@Nullable @Assisted String message) {
+ super(projectControlFactory, groupBackend, metaDataUpdateFactory,
+ projectName, base, sectionList, message, true);
this.projectAccessFactory = projectAccessFactory;
- this.projectControlFactory = projectControlFactory;
this.projectCache = projectCache;
- this.groupCache = groupCache;
- this.metaDataUpdateFactory = metaDataUpdateFactory;
-
- this.projectName = projectName;
- this.base = base;
- this.sectionList = sectionList;
- this.message = message;
}
@Override
- public ProjectAccess call() throws NoSuchProjectException, IOException,
- ConfigInvalidException, InvalidNameException, NoSuchGroupException,
- OrmConcurrencyException {
- final ProjectControl projectControl =
- projectControlFactory.controlFor(projectName);
-
- final MetaDataUpdate md;
- try {
- md = metaDataUpdateFactory.create(projectName);
- } catch (RepositoryNotFoundException notFound) {
- throw new NoSuchProjectException(projectName);
- }
- try {
- ProjectConfig config = ProjectConfig.read(md, base);
- Set<String> toDelete = scanSectionNames(config);
-
- for (AccessSection section : mergeSections(sectionList)) {
- String name = section.getName();
-
- if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
- if (!projectControl.isOwner()) {
- continue;
- }
- replace(config, toDelete, section);
-
- } else if (AccessSection.isValid(name)) {
- if (!projectControl.controlForRef(name).isOwner()) {
- continue;
- }
-
- RefControl.validateRefPattern(name);
-
- replace(config, toDelete, section);
- }
- }
-
- for (String name : toDelete) {
- if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
- if (projectControl.isOwner()) {
- config.remove(config.getAccessSection(name));
- }
-
- } else if (projectControl.controlForRef(name).isOwner()) {
- config.remove(config.getAccessSection(name));
- }
- }
-
- if (message != null && !message.isEmpty()) {
- if (!message.endsWith("\n")) {
- message += "\n";
- }
- md.setMessage(message);
- } else {
- md.setMessage("Modify access rules\n");
- }
-
- if (config.commit(md)) {
- projectCache.evict(config.getProject());
- return projectAccessFactory.create(projectName).call();
-
- } else {
- throw new OrmConcurrencyException("Cannot update " + projectName);
- }
- } finally {
- md.close();
- }
- }
-
- private void replace(ProjectConfig config, Set<String> toDelete,
- AccessSection section) throws NoSuchGroupException {
- for (Permission permission : section.getPermissions()) {
- for (PermissionRule rule : permission.getRules()) {
- lookupGroup(rule);
- }
- }
- config.replace(section);
- toDelete.remove(section.getName());
- }
-
- private static List<AccessSection> mergeSections(List<AccessSection> src) {
- Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
- for (AccessSection section : src) {
- if (section.getPermissions().isEmpty()) {
- continue;
- }
-
- AccessSection prior = map.get(section.getName());
- if (prior != null) {
- prior.mergeFrom(section);
- } else {
- map.put(section.getName(), section);
- }
- }
- return new ArrayList<AccessSection>(map.values());
- }
-
- private static Set<String> scanSectionNames(ProjectConfig config) {
- Set<String> names = new HashSet<String>();
- for (AccessSection section : config.getAccessSections()) {
- names.add(section.getName());
- }
- return names;
- }
-
- private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
- GroupReference ref = rule.getGroup();
- if (ref.getUUID() == null) {
- AccountGroup.NameKey name = new AccountGroup.NameKey(ref.getName());
- AccountGroup group = groupCache.get(name);
- if (group == null) {
- throw new NoSuchGroupException(name);
- }
- ref.setUUID(group.getGroupUUID());
- }
+ protected ProjectAccess updateProjectConfig(ProjectConfig config,
+ MetaDataUpdate md) throws IOException, NoSuchProjectException, ConfigInvalidException {
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ return projectAccessFactory.create(projectName).call();
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index bd6e460cde..41354aa215 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -65,7 +65,8 @@ class ChangeProjectSettings extends Handler<ProjectDetail> {
}
@Override
- public ProjectDetail call() throws NoSuchProjectException, OrmException {
+ public ProjectDetail call() throws NoSuchProjectException, OrmException,
+ IOException {
final Project.NameKey projectName = update.getNameKey();
projectControlFactory.ownerFor(projectName);
@@ -74,6 +75,8 @@ class ChangeProjectSettings extends Handler<ProjectDetail> {
md = metaDataUpdateFactory.create(projectName);
} catch (RepositoryNotFoundException notFound) {
throw new NoSuchProjectException(projectName);
+ } catch (IOException e) {
+ throw new OrmException(e);
}
try {
// TODO We really should take advantage of the Git commit DAG and
@@ -83,10 +86,11 @@ class ChangeProjectSettings extends Handler<ProjectDetail> {
config.getProject().copySettingsFrom(update);
md.setMessage("Modified project settings\n");
- if (config.commit(md)) {
+ try {
+ config.commit(md);
mgr.setProjectDescription(projectName, update.getDescription());
userCache.get().evict(config.getProject());
- } else {
+ } catch (IOException e) {
throw new OrmConcurrencyException("Cannot update " + projectName);
}
} catch (ConfigInvalidException err) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
index 039a301fc8..141f33234a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
@@ -26,7 +26,7 @@ import com.google.gwtjsonrpc.common.VoidResult;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import org.eclipse.jgit.lib.Constants;
+import java.util.Collections;
public class CreateProjectHandler extends Handler<VoidResult> {
@@ -74,7 +74,7 @@ public class CreateProjectHandler extends Handler<VoidResult> {
}
args.projectDescription = "";
args.submitType = SubmitType.MERGE_IF_NECESSARY;
- args.branch = Constants.MASTER;
+ args.branch = Collections.emptyList();
args.createEmptyCommit = emptyCommit;
args.permissionsOnly = permissionsOnly;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index 073e2d761e..f2b3ca3dd2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -18,11 +18,13 @@ import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -34,6 +36,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.Set;
class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@@ -47,9 +50,10 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> {
private final ProjectControl.Factory projectControlFactory;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final IdentifiedUser identifiedUser;
private final ChangeHooks hooks;
+ private final ReviewDb db;
private final Project.NameKey projectName;
private final Set<Branch.NameKey> toRemove;
@@ -57,9 +61,10 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@Inject
DeleteBranches(final ProjectControl.Factory projectControlFactory,
final GitRepositoryManager repoManager,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
final IdentifiedUser identifiedUser,
final ChangeHooks hooks,
+ final ReviewDb db,
@Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
this.projectControlFactory = projectControlFactory;
@@ -67,6 +72,7 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> {
this.replication = replication;
this.identifiedUser = identifiedUser;
this.hooks = hooks;
+ this.db = db;
this.projectName = name;
this.toRemove = toRemove;
@@ -74,17 +80,23 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@Override
public Set<Branch.NameKey> call() throws NoSuchProjectException,
- RepositoryNotFoundException {
+ RepositoryNotFoundException, OrmException, IOException {
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
- for (Branch.NameKey k : toRemove) {
+ final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
+ while (branchIt.hasNext()) {
+ final Branch.NameKey k = branchIt.next();
if (!projectName.equals(k.getParentKey())) {
throw new IllegalArgumentException("All keys must be from same project");
}
if (!projectControl.controlForRef(k).canDelete()) {
throw new IllegalStateException("Cannot delete " + k.getShortName());
}
+
+ if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
+ branchIt.remove();
+ }
}
final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
@@ -109,7 +121,7 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> {
case FAST_FORWARD:
case FORCED:
deleted.add(branchKey);
- replication.scheduleUpdate(projectName, refname);
+ replication.fire(projectName, refname);
hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
break;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 68fde4511e..2366423bce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -62,7 +62,7 @@ class ListBranches extends Handler<ListBranchesResult> {
}
@Override
- public ListBranchesResult call() throws NoSuchProjectException {
+ public ListBranchesResult call() throws NoSuchProjectException, IOException {
final ProjectControl pctl = projectControlFactory.validateFor( //
projectName, //
ProjectControl.OWNER | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
new file mode 100644
index 0000000000..d327d35975
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
@@ -0,0 +1,70 @@
+// 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.httpd.rpc.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.project.ListProjects;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListProjectsServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private final ParameterParser paramParser;
+ private final Provider<ListProjects> factory;
+
+ @Inject
+ ListProjectsServlet(final Provider<CurrentUser> currentUser,
+ ParameterParser paramParser, Provider<ListProjects> ls) {
+ super(currentUser);
+ this.paramParser = paramParser;
+ this.factory = ls;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ ListProjects impl = factory.get();
+ if (!Strings.isNullOrEmpty(req.getPathInfo())) {
+ impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
+ }
+ if (acceptsJson(req)) {
+ impl.setFormat(OutputFormat.JSON_COMPACT);
+ }
+ if (paramParser.parse(impl, req, res)) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ if (impl.getFormat().isJson()) {
+ res.setContentType(JSON_TYPE);
+ buf.write(JSON_MAGIC);
+ } else {
+ res.setContentType("text/plain");
+ }
+ impl.display(buf);
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 7ac4ec38c5..7b3b8e7017 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -23,7 +23,7 @@ import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -51,7 +51,7 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
ProjectAccessFactory create(@Assisted Project.NameKey name);
}
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final ProjectCache projectCache;
private final ProjectControl.Factory projectControlFactory;
private final GroupControl.Factory groupControlFactory;
@@ -62,7 +62,7 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
private ProjectControl pc;
@Inject
- ProjectAccessFactory(final GroupCache groupCache,
+ ProjectAccessFactory(final GroupBackend groupBackend,
final ProjectCache projectCache,
final ProjectControl.Factory projectControlFactory,
final GroupControl.Factory groupControlFactory,
@@ -70,7 +70,7 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
final AllProjectsName allProjectsName,
@Assisted final Project.NameKey name) {
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.projectCache = projectCache;
this.projectControlFactory = projectControlFactory;
this.groupControlFactory = groupControlFactory;
@@ -94,12 +94,11 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
try {
config = ProjectConfig.read(md);
- if (config.updateGroupNames(groupCache)) {
+ if (config.updateGroupNames(groupBackend)) {
md.setMessage("Update group names\n");
- if (config.commit(md)) {
- projectCache.evict(config.getProject());
- pc = open();
- }
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ pc = open();
} else if (config.getRevision() != null
&& !config.getRevision().equals(
pc.getProjectState().getConfig().getRevision())) {
@@ -110,6 +109,7 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
md.close();
}
+ final RefControl metaConfigControl = pc.controlForRef(GitRepositoryManager.REF_CONFIG);
List<AccessSection> local = new ArrayList<AccessSection>();
Set<String> ownerOf = new HashSet<String>();
Map<AccountGroup.UUID, Boolean> visibleGroups =
@@ -129,6 +129,9 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
local.add(section);
ownerOf.add(name);
+ } else if (metaConfigControl.isVisible()) {
+ local.add(section);
+
} else if (rc.isVisible()) {
// Filter the section to only add rules describing groups that
// are visible to the current-user. This includes any group the
@@ -195,8 +198,9 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
detail.setLocal(local);
detail.setOwnerOf(ownerOf);
- detail.setConfigVisible(pc.isOwner()
- || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
+ detail.setCanUpload(pc.isOwner()
+ || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+ detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
return detail;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
new file mode 100644
index 0000000000..02b84b0f7f
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -0,0 +1,190 @@
+// 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.httpd.rpc.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class ProjectAccessHandler<T> extends Handler<T> {
+
+ private final ProjectControl.Factory projectControlFactory;
+ protected final GroupBackend groupBackend;
+ private final MetaDataUpdate.User metaDataUpdateFactory;
+
+ protected final Project.NameKey projectName;
+ protected final ObjectId base;
+ private List<AccessSection> sectionList;
+ protected String message;
+ private boolean checkIfOwner;
+
+ protected ProjectAccessHandler(
+ final ProjectControl.Factory projectControlFactory,
+ final GroupBackend groupBackend,
+ final MetaDataUpdate.User metaDataUpdateFactory,
+ final Project.NameKey projectName, final ObjectId base,
+ final List<AccessSection> sectionList, final String message,
+ final boolean checkIfOwner) {
+ this.projectControlFactory = projectControlFactory;
+ this.groupBackend = groupBackend;
+ this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+ this.projectName = projectName;
+ this.base = base;
+ this.sectionList = sectionList;
+ this.message = message;
+ this.checkIfOwner = checkIfOwner;
+ }
+
+ @Override
+ public final T call() throws NoSuchProjectException, IOException,
+ ConfigInvalidException, InvalidNameException, NoSuchGroupException,
+ OrmException {
+ final ProjectControl projectControl =
+ projectControlFactory.controlFor(projectName);
+
+ final MetaDataUpdate md;
+ try {
+ md = metaDataUpdateFactory.create(projectName);
+ } catch (RepositoryNotFoundException notFound) {
+ throw new NoSuchProjectException(projectName);
+ }
+ try {
+ ProjectConfig config = ProjectConfig.read(md, base);
+ Set<String> toDelete = scanSectionNames(config);
+
+ for (AccessSection section : mergeSections(sectionList)) {
+ String name = section.getName();
+
+ if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+ if (checkIfOwner && !projectControl.isOwner()) {
+ continue;
+ }
+ replace(config, toDelete, section);
+
+ } else if (AccessSection.isValid(name)) {
+ if (checkIfOwner && !projectControl.controlForRef(name).isOwner()) {
+ continue;
+ }
+
+ RefControl.validateRefPattern(name);
+
+ replace(config, toDelete, section);
+ }
+ }
+
+ for (String name : toDelete) {
+ if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+ if (!checkIfOwner || projectControl.isOwner()) {
+ config.remove(config.getAccessSection(name));
+ }
+
+ } else if (!checkIfOwner || projectControl.controlForRef(name).isOwner()) {
+ config.remove(config.getAccessSection(name));
+ }
+ }
+
+ if (message != null && !message.isEmpty()) {
+ if (!message.endsWith("\n")) {
+ message += "\n";
+ }
+ md.setMessage(message);
+ } else {
+ md.setMessage("Modify access rules\n");
+ }
+
+ return updateProjectConfig(config, md);
+ } finally {
+ md.close();
+ }
+ }
+
+ protected abstract T updateProjectConfig(ProjectConfig config,
+ MetaDataUpdate md) throws IOException, NoSuchProjectException,
+ ConfigInvalidException, OrmException;
+
+ private void replace(ProjectConfig config, Set<String> toDelete,
+ AccessSection section) throws NoSuchGroupException {
+ for (Permission permission : section.getPermissions()) {
+ for (PermissionRule rule : permission.getRules()) {
+ lookupGroup(rule);
+ }
+ }
+ config.replace(section);
+ toDelete.remove(section.getName());
+ }
+
+ private static List<AccessSection> mergeSections(List<AccessSection> src) {
+ Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
+ for (AccessSection section : src) {
+ if (section.getPermissions().isEmpty()) {
+ continue;
+ }
+
+ AccessSection prior = map.get(section.getName());
+ if (prior != null) {
+ prior.mergeFrom(section);
+ } else {
+ map.put(section.getName(), section);
+ }
+ }
+ return new ArrayList<AccessSection>(map.values());
+ }
+
+ private static Set<String> scanSectionNames(ProjectConfig config) {
+ Set<String> names = new HashSet<String>();
+ for (AccessSection section : config.getAccessSections()) {
+ names.add(section.getName());
+ }
+ return names;
+ }
+
+ private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
+ GroupReference ref = rule.getGroup();
+ if (ref.getUUID() == null) {
+ final GroupReference group =
+ GroupBackends.findBestSuggestion(groupBackend, ref.getName());
+ if (group == null) {
+ throw new NoSuchGroupException(ref.getName());
+ }
+ ref.setUUID(group.getUUID());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index a6bb74fb62..983f1fcd2e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -19,8 +19,8 @@ import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.data.ProjectAdminService;
import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.common.data.ProjectList;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -34,44 +34,36 @@ import java.util.Set;
class ProjectAdminServiceImpl implements ProjectAdminService {
private final AddBranch.Factory addBranchFactory;
private final ChangeProjectAccess.Factory changeProjectAccessFactory;
+ private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
private final DeleteBranches.Factory deleteBranchesFactory;
private final ListBranches.Factory listBranchesFactory;
- private final VisibleProjects.Factory visibleProjectsFactory;
private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
private final ProjectAccessFactory.Factory projectAccessFactory;
private final CreateProjectHandler.Factory createProjectHandlerFactory;
private final ProjectDetailFactory.Factory projectDetailFactory;
- private final SuggestParentCandidatesHandler.Factory suggestParentCandidatesHandlerFactory;
@Inject
ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
final ChangeProjectAccess.Factory changeProjectAccessFactory,
+ final ReviewProjectAccess.Factory reviewProjectAccessFactory,
final ChangeProjectSettings.Factory changeProjectSettingsFactory,
final DeleteBranches.Factory deleteBranchesFactory,
final ListBranches.Factory listBranchesFactory,
- final VisibleProjects.Factory visibleProjectsFactory,
final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
final ProjectAccessFactory.Factory projectAccessFactory,
final ProjectDetailFactory.Factory projectDetailFactory,
- final SuggestParentCandidatesHandler.Factory parentCandidatesFactory,
final CreateProjectHandler.Factory createNewProjectFactory) {
this.addBranchFactory = addBranchFactory;
this.changeProjectAccessFactory = changeProjectAccessFactory;
+ this.reviewProjectAccessFactory = reviewProjectAccessFactory;
this.changeProjectSettingsFactory = changeProjectSettingsFactory;
this.deleteBranchesFactory = deleteBranchesFactory;
this.listBranchesFactory = listBranchesFactory;
- this.visibleProjectsFactory = visibleProjectsFactory;
this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
this.projectAccessFactory = projectAccessFactory;
this.projectDetailFactory = projectDetailFactory;
this.createProjectHandlerFactory = createNewProjectFactory;
- this.suggestParentCandidatesHandlerFactory = parentCandidatesFactory;
- }
-
- @Override
- public void visibleProjects(final AsyncCallback<ProjectList> callback) {
- visibleProjectsFactory.create().to(callback);
}
@Override
@@ -80,11 +72,6 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
}
@Override
- public void suggestParentCandidates(AsyncCallback<List<Project>> callback) {
- suggestParentCandidatesHandlerFactory.create().to(callback);
- }
-
- @Override
public void projectDetail(final Project.NameKey projectName,
final AsyncCallback<ProjectDetail> callback) {
projectDetailFactory.create(projectName).to(callback);
@@ -102,17 +89,25 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
changeProjectSettingsFactory.create(update).to(callback);
}
+ private static ObjectId getBase(final String baseRevision) {
+ if (baseRevision != null && !baseRevision.isEmpty()) {
+ return ObjectId.fromString(baseRevision);
+ }
+ return null;
+ }
+
@Override
public void changeProjectAccess(Project.NameKey projectName,
String baseRevision, String msg, List<AccessSection> sections,
AsyncCallback<ProjectAccess> cb) {
- ObjectId base;
- if (baseRevision != null && !baseRevision.isEmpty()) {
- base = ObjectId.fromString(baseRevision);
- } else {
- base = null;
- }
- changeProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
+ changeProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
+ }
+
+ @Override
+ public void reviewProjectAccess(Project.NameKey projectName,
+ String baseRevision, String msg, List<AccessSection> sections,
+ AsyncCallback<Change.Id> cb) {
+ reviewProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
}
@Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index d782da41a1..27416409bd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -51,7 +51,7 @@ class ProjectDetailFactory extends Handler<ProjectDetail> {
}
@Override
- public ProjectDetail call() throws NoSuchProjectException {
+ public ProjectDetail call() throws NoSuchProjectException, IOException {
final ProjectControl pc =
projectControlFactory.validateFor(projectName, ProjectControl.OWNER
| ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2eb55b3d81..e943e3fce1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -30,15 +30,14 @@ public class ProjectModule extends RpcServletModule {
protected void configure() {
factory(AddBranch.Factory.class);
factory(ChangeProjectAccess.Factory.class);
+ factory(ReviewProjectAccess.Factory.class);
factory(CreateProjectHandler.Factory.class);
factory(ChangeProjectSettings.Factory.class);
factory(DeleteBranches.Factory.class);
factory(ListBranches.Factory.class);
- factory(VisibleProjects.Factory.class);
factory(VisibleProjectDetails.Factory.class);
factory(ProjectAccessFactory.Factory.class);
factory(ProjectDetailFactory.Factory.class);
- factory(SuggestParentCandidatesHandler.Factory.class);
}
});
rpc(ProjectAdminServiceImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
new file mode 100644
index 0000000000..69a283a783
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -0,0 +1,147 @@
+// 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.httpd.rpc.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+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.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+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.patch.AddReviewer;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
+ interface Factory {
+ ReviewProjectAccess create(@Assisted Project.NameKey projectName,
+ @Nullable @Assisted ObjectId base,
+ @Assisted List<AccessSection> sectionList,
+ @Nullable @Assisted String message);
+ }
+
+ private final ReviewDb db;
+ private final IdentifiedUser user;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final AddReviewer.Factory addReviewerFactory;
+
+ @Inject
+ ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
+ final GroupBackend groupBackend,
+ final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
+ final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
+ final AddReviewer.Factory addReviewerFactory,
+
+ @Assisted final Project.NameKey projectName,
+ @Nullable @Assisted final ObjectId base,
+ @Assisted List<AccessSection> sectionList,
+ @Nullable @Assisted String message) {
+ super(projectControlFactory, groupBackend, metaDataUpdateFactory,
+ projectName, base, sectionList, message, false);
+ this.db = db;
+ this.user = user;
+ this.patchSetInfoFactory = patchSetInfoFactory;
+ this.addReviewerFactory = addReviewerFactory;
+ }
+
+ @Override
+ protected Change.Id updateProjectConfig(ProjectConfig config, MetaDataUpdate md)
+ throws IOException, OrmException {
+ Change.Id changeId = new Change.Id(db.nextChangeId());
+ PatchSet ps = new PatchSet(new PatchSet.Id(changeId, 1));
+ RevCommit commit = config.commitToNewRef(md, ps.getRefName());
+ if (commit.getId().equals(base)) {
+ return null;
+ }
+
+ Change change = new Change(
+ new Change.Key("I" + commit.name()),
+ changeId,
+ user.getAccountId(),
+ new Branch.NameKey(
+ config.getProject().getNameKey(),
+ GitRepositoryManager.REF_CONFIG));
+ change.nextPatchSetId();
+
+ ps.setCreatedOn(change.getCreatedOn());
+ ps.setUploader(change.getOwner());
+ ps.setRevision(new RevId(commit.name()));
+
+ PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
+ change.setCurrentPatchSet(info);
+ ChangeUtil.updated(change);
+
+ db.changes().beginTransaction(changeId);
+ try {
+ insertAncestors(ps.getId(), commit);
+ db.patchSets().insert(Collections.singleton(ps));
+ db.changes().insert(Collections.singleton(change));
+ addProjectOwnersAsReviewers(changeId);
+ db.commit();
+ } finally {
+ db.rollback();
+ }
+ return changeId;
+ }
+
+ private void insertAncestors(PatchSet.Id id, RevCommit src)
+ throws OrmException {
+ final int cnt = src.getParentCount();
+ List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+ for (int p = 0; p < cnt; p++) {
+ PatchSetAncestor a;
+
+ a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
+ a.setAncestorRevision(new RevId(src.getParent(p).name()));
+ toInsert.add(a);
+ }
+ db.patchSetAncestors().insert(toInsert);
+ }
+
+ private void addProjectOwnersAsReviewers(final Change.Id changeId) {
+ final String projectOwners =
+ groupBackend.get(AccountGroup.PROJECT_OWNERS).getName();
+ try {
+ addReviewerFactory.create(changeId, Collections.singleton(projectOwners),
+ false).call();
+ } catch (Exception e) {
+ // one of the owner groups is not visible to the user and this it why it
+ // can't be added as reviewer
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
deleted file mode 100644
index ba0e4cd334..0000000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2011 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.httpd.rpc.project;
-
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.inject.Inject;
-
-import java.util.List;
-
-public class SuggestParentCandidatesHandler extends Handler<List<Project>> {
- interface Factory {
- SuggestParentCandidatesHandler create();
- }
-
- private final SuggestParentCandidates suggestParentCandidates;
-
- @Inject
- SuggestParentCandidatesHandler(final SuggestParentCandidates suggestParentCandidates) {
- this.suggestParentCandidates = suggestParentCandidates;
- }
-
- @Override
- public List<Project> call() throws Exception {
- return suggestParentCandidates.getProjects();
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
index e12cfb16fc..1c22d83d0b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
@@ -14,7 +14,6 @@
package com.google.gerrit.httpd.rpc.project;
-
import com.google.gerrit.common.data.ProjectDetail;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Project;
@@ -22,6 +21,7 @@ import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -50,6 +50,7 @@ class VisibleProjectDetails extends Handler<List<ProjectDetail>> {
try {
result.add(projectDetailFactory.create(projectName).call());
} catch (NoSuchProjectException e) {
+ } catch (IOException e) {
}
}
Collections.sort(result, new Comparator<ProjectDetail>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
deleted file mode 100644
index ba65617aa7..0000000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.inject.Inject;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class VisibleProjects extends Handler<ProjectList> {
- interface Factory {
- VisibleProjects create();
- }
-
- private final ProjectControl.Factory projectControlFactory;
- private final ProjectCache projectCache;
- private final CurrentUser user;
-
- @Inject
- VisibleProjects(final ProjectControl.Factory projectControlFactory,
- final ProjectCache projectCache, final CurrentUser user) {
- this.projectControlFactory = projectControlFactory;
- this.projectCache = projectCache;
- this.user = user;
- }
-
- @Override
- public ProjectList call() {
- ProjectList result = new ProjectList();
- result.setProjects(getProjects());
- result.setCanCreateProject(user.getCapabilities().canCreateProject());
- return result;
- }
-
- private List<Project> getProjects() {
- List<Project> result = new ArrayList<Project>();
- for (Project.NameKey p : projectCache.all()) {
- try {
- ProjectControl c = projectControlFactory.controlFor(p);
- if (c.isVisible() || c.isOwner()) {
- result.add(c.getProject());
- }
- } catch (NoSuchProjectException e) {
- continue;
- }
- }
- Collections.sort(result, new Comparator<Project>() {
- public int compare(final Project a, final Project b) {
- return a.getName().compareTo(b.getName());
- }
- });
- return result;
- }
-}
diff --git a/gerrit-launcher/.gitignore b/gerrit-launcher/.gitignore
index 194bedcbc4..980a6b1722 100644
--- a/gerrit-launcher/.gitignore
+++ b/gerrit-launcher/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-launcher.iml \ No newline at end of file
diff --git a/gerrit-launcher/.settings/org.eclipse.core.resources.prefs b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
index c780f4418c..e9441bb123 100644
--- a/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 7d07652f05..e700351517 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 7f2007ef56..9cca559f75 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -31,9 +31,10 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.Enumeration;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -196,7 +197,7 @@ public final class GerritLauncher {
throw e;
}
- final ArrayList<URL> jars = new ArrayList<URL>();
+ final SortedMap<String, URL> jars = new TreeMap<String, URL>();
try {
final ZipFile zf = new ZipFile(path);
try {
@@ -208,6 +209,7 @@ public final class GerritLauncher {
}
if (ze.getName().startsWith("WEB-INF/lib/")) {
+ String name = ze.getName().substring("WEB-INF/lib/".length());
final File tmp = createTempFile(safeName(ze), ".jar");
final FileOutputStream out = new FileOutputStream(tmp);
try {
@@ -224,7 +226,7 @@ public final class GerritLauncher {
} finally {
out.close();
}
- jars.add(tmp.toURI().toURL());
+ jars.put(name, tmp.toURI().toURL());
}
}
} finally {
@@ -237,13 +239,38 @@ public final class GerritLauncher {
if (jars.isEmpty()) {
return GerritLauncher.class.getClassLoader();
}
- Collections.sort(jars, new Comparator<URL>() {
- public int compare(URL o1, URL o2) {
- return o1.toString().compareTo(o2.toString());
- }
- });
- return new URLClassLoader(jars.toArray(new URL[jars.size()]));
+ // The extension API needs to be its own ClassLoader, along
+ // with a few of its dependencies. Try to construct this first.
+ List<URL> extapi = new ArrayList<URL>();
+ move(jars, "gerrit-extension-api-", extapi);
+ move(jars, "guice-", extapi);
+ move(jars, "javax.inject-1.jar", extapi);
+ move(jars, "aopalliance-1.0.jar", extapi);
+ move(jars, "guice-servlet-", extapi);
+ move(jars, "servlet-api-", extapi);
+
+ ClassLoader parent = ClassLoader.getSystemClassLoader();
+ if (!extapi.isEmpty()) {
+ parent = new URLClassLoader(
+ extapi.toArray(new URL[extapi.size()]),
+ parent);
+ }
+ return new URLClassLoader(
+ jars.values().toArray(new URL[jars.size()]),
+ parent);
+ }
+
+ private static void move(SortedMap<String, URL> jars,
+ String prefix,
+ List<URL> extapi) {
+ SortedMap<String, URL> matches = jars.tailMap(prefix);
+ if (!matches.isEmpty()) {
+ String first = matches.firstKey();
+ if (first.startsWith(prefix)) {
+ extapi.add(jars.remove(first));
+ }
+ }
}
private static String safeName(final ZipEntry ze) {
@@ -423,36 +450,42 @@ public final class GerritLauncher {
}
private static File tmproot() {
- // Try to find the user's home directory. If we can't find it
- // return null so the JVM's default temporary directory is used
- // instead. This is probably /tmp or /var/tmp.
- //
- String userHome = System.getProperty("user.home");
- if (userHome == null || "".equals(userHome)) {
- userHome = System.getenv("HOME");
+ File tmp;
+ String gerritTemp = System.getenv("GERRIT_TMP");
+ if (gerritTemp != null && gerritTemp.length() > 0) {
+ tmp = new File(gerritTemp);
+ } else {
+ // Try to find the user's home directory. If we can't find it
+ // return null so the JVM's default temporary directory is used
+ // instead. This is probably /tmp or /var/tmp.
+ //
+ String userHome = System.getProperty("user.home");
if (userHome == null || "".equals(userHome)) {
- System.err.println("warning: cannot determine home directory");
- System.err.println("warning: using system temporary directory instead");
- return null;
+ userHome = System.getenv("HOME");
+ if (userHome == null || "".equals(userHome)) {
+ System.err.println("warning: cannot determine home directory");
+ System.err.println("warning: using system temporary directory instead");
+ return null;
+ }
}
- }
- // Ensure the home directory exists. If it doesn't, try to make it.
- //
- final File home = new File(userHome);
- if (!home.exists()) {
- if (home.mkdirs()) {
- System.err.println("warning: created " + home.getAbsolutePath());
- } else {
- System.err.println("warning: " + home.getAbsolutePath() + " not found");
- System.err.println("warning: using system temporary directory instead");
- return null;
+ // Ensure the home directory exists. If it doesn't, try to make it.
+ //
+ final File home = new File(userHome);
+ if (!home.exists()) {
+ if (home.mkdirs()) {
+ System.err.println("warning: created " + home.getAbsolutePath());
+ } else {
+ System.err.println("warning: " + home.getAbsolutePath() + " not found");
+ System.err.println("warning: using system temporary directory instead");
+ return null;
+ }
}
- }
- // Use $HOME/.gerritcodereview/tmp for our temporary file area.
- //
- final File tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+ // Use $HOME/.gerritcodereview/tmp for our temporary file area.
+ //
+ tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+ }
if (!tmp.exists() && !tmp.mkdirs()) {
System.err.println("warning: cannot create " + tmp.getAbsolutePath());
System.err.println("warning: using system temporary directory instead");
diff --git a/gerrit-main/.gitignore b/gerrit-main/.gitignore
index 194bedcbc4..c847710ec9 100644
--- a/gerrit-main/.gitignore
+++ b/gerrit-main/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-main.iml \ No newline at end of file
diff --git a/gerrit-main/.settings/org.eclipse.core.resources.prefs b/gerrit-main/.settings/org.eclipse.core.resources.prefs
index c780f4418c..e9441bb123 100644
--- a/gerrit-main/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-main/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 9d8320c305..bb2d76371b 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.gitignore b/gerrit-openid/.gitignore
index 194bedcbc4..158faf191e 100644
--- a/gerrit-openid/.gitignore
+++ b/gerrit-openid/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-openid.iml \ No newline at end of file
diff --git a/gerrit-openid/.settings/org.eclipse.core.resources.prefs b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
index fc11c3fe6f..f9fe34593f 100644
--- a/gerrit-openid/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/test/java=UTF-8
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index ed2625e2c3..01e2e3ec26 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-openid</artifactId>
@@ -51,8 +51,7 @@ limitations under the License.
<dependency>
<groupId>org.openid4java</groupId>
- <artifactId>openid4java-consumer</artifactId>
- <type>pom</type>
+ <artifactId>openid4java</artifactId>
</dependency>
<dependency>
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 0593bce94f..09a5d1043b 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.ConfigUtil;
@@ -416,7 +417,7 @@ class OpenIdServiceImpl implements OpenIdService {
lastId.setMaxAge(0);
}
rsp.addCookie(lastId);
- webSession.get().login(arsp, remember);
+ webSession.get().login(arsp, AuthMethod.COOKIE, remember);
if (arsp.isNew() && claimedIdentifier != null) {
final com.google.gerrit.server.account.AuthRequest linkReq =
new com.google.gerrit.server.account.AuthRequest(
@@ -430,7 +431,7 @@ class OpenIdServiceImpl implements OpenIdService {
case LINK_IDENTIY: {
arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
- webSession.get().login(arsp, remember);
+ webSession.get().login(arsp, AuthMethod.COOKIE, remember);
callback(false, req, rsp);
break;
}
diff --git a/gerrit-package-plugins/.gitignore b/gerrit-package-plugins/.gitignore
new file mode 100644
index 0000000000..c96b05c166
--- /dev/null
+++ b/gerrit-package-plugins/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-package-plugins.iml
diff --git a/gerrit-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
new file mode 100644
index 0000000000..c072719bc1
--- /dev/null
+++ b/gerrit-package-plugins/pom.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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>
+
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-package-plugins</artifactId>
+ <packaging>war</packaging>
+ <version>2.5-SNAPSHOT</version>
+
+ <name>Gerrit Code Review - Package Plugins</name>
+ <url>http://code.google.com/p/gerrit/</url>
+
+ <properties>
+ <project.build.sourceEncoding>
+ UTF-8
+ </project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-war</artifactId>
+ <version>${project.version}</version>
+ <type>war</type>
+ </dependency>
+ <dependency>
+ <groupId>com.googlesource.gerrit.plugins.replication</groupId>
+ <artifactId>replication</artifactId>
+ <version>1.0</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <version>2.1</version>
+ <executions>
+ <execution>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>copy-dependencies</goal>
+ </goals>
+ <configuration>
+ <includeTypes>jar</includeTypes>
+ <stripVersion>true</stripVersion>
+ <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-war-plugin</artifactId>
+ <version>2.1.1</version>
+ <configuration>
+ <warName>gerrit-full-${project.version}</warName>
+ <archive>
+ <addMavenDescriptor>false</addMavenDescriptor>
+ <manifestEntries>
+ <Main-Class>Main</Main-Class>
+ <Implementation-Title>Gerrit Code Review</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/gerrit-patch-commonsnet/.gitignore b/gerrit-patch-commonsnet/.gitignore
index 194bedcbc4..121f8e9014 100644
--- a/gerrit-patch-commonsnet/.gitignore
+++ b/gerrit-patch-commonsnet/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-commonsnet.iml \ No newline at end of file
diff --git a/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
index 589908f32c..e9441bb123 100644
--- a/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 75ee12ed8f..f1a8b3e763 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index 7d7bc49c4c..1f08a81deb 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -18,7 +18,11 @@ import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
import org.apache.commons.codec.binary.Base64;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.security.InvalidKeyException;
@@ -50,7 +54,22 @@ public class AuthSMTPClient extends SMTPClient {
}
_socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
- _connectAction_();
+
+ // XXX: Can't call _connectAction_() because SMTP server doesn't
+ // give banner information again after STARTTLS, thus SMTP._connectAction_()
+ // will wait on __getReply() forever, see source code of commons-net-2.2.
+ //
+ // The lines below are copied from SocketClient._connectAction_() and
+ // SMTP._connectAction_() in commons-net-2.2.
+ _socket_.setSoTimeout(_timeout_);
+ _input_ = _socket_.getInputStream();
+ _output_ = _socket_.getOutputStream();
+ _reader =
+ new BufferedReader(new InputStreamReader(_input_,
+ UTF_8));
+ _writer =
+ new BufferedWriter(new OutputStreamWriter(_output_,
+ UTF_8));
return true;
}
diff --git a/gerrit-patch-jgit/.gitignore b/gerrit-patch-jgit/.gitignore
index 194bedcbc4..7c4c4334f8 100644
--- a/gerrit-patch-jgit/.gitignore
+++ b/gerrit-patch-jgit/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-jgit.iml \ No newline at end of file
diff --git a/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
index 589908f32c..e9441bb123 100644
--- a/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f8190f5aab..65223fb530 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
index 1df89b7d9d..9a55e0fd7d 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
@@ -76,7 +76,7 @@ public class EditDeserializer implements JsonDeserializer<Edit>,
public JsonElement serialize(final Edit src, final Type typeOfSrc,
final JsonSerializationContext context) {
if (src == null) {
- return new JsonNull();
+ return JsonNull.INSTANCE;
}
final JsonArray a = new JsonArray();
add(a, src);
diff --git a/gerrit-pgm/.gitignore b/gerrit-pgm/.gitignore
index 194bedcbc4..dafe3551e0 100644
--- a/gerrit-pgm/.gitignore
+++ b/gerrit-pgm/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-pgm.iml \ No newline at end of file
diff --git a/gerrit-pgm/.settings/org.eclipse.core.resources.prefs b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
index 9df523e4f6..839d647eef 100644
--- a/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 1463d15598..a01521932f 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 25b7699a68..a826c88b81 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -17,13 +17,16 @@ package com.google.gerrit.pgm;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
+import com.google.gerrit.httpd.AllRequestFilter;
import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.httpd.GitOverHttpModule;
import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.gerrit.httpd.RequestContextFilter;
+import com.google.gerrit.httpd.SignedTokenRestTokenVerifier;
import com.google.gerrit.httpd.WebModule;
import com.google.gerrit.httpd.WebSshGlueModule;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.http.jetty.GetUserFilter;
import com.google.gerrit.pgm.http.jetty.JettyEnv;
@@ -34,27 +37,40 @@ import com.google.gerrit.pgm.util.LogFileCompressor;
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.MasterNodeStartup;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
-import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.schema.SchemaUpdater;
import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.UpdateUI;
import com.google.gerrit.server.ssh.NoSshModule;
import com.google.gerrit.sshd.SshModule;
import com.google.gerrit.sshd.commands.MasterCommandModule;
import com.google.gerrit.sshd.commands.SlaveCommandModule;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Provider;
+import org.eclipse.jgit.lib.Config;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -140,6 +156,9 @@ public class Daemon extends SiteProgram {
dbInjector = createDbInjector(MULTI_USER);
cfgInjector = createCfgInjector();
sysInjector = createSysInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setCfgInjector(cfgInjector);
+ sysInjector.getInstance(SchemaUpgrade.class).upgradeSchema();
manager.add(dbInjector, cfgInjector, sysInjector);
if (sshd) {
@@ -152,6 +171,7 @@ public class Daemon extends SiteProgram {
manager.start();
RuntimeShutdown.add(new Runnable() {
+ @Override
public void run() {
log.info("caught shutdown, cleaning up");
if (runId != null) {
@@ -186,6 +206,74 @@ public class Daemon extends SiteProgram {
}
}
+ static class SchemaUpgrade {
+
+ private final Config config;
+ private final SchemaUpdater updater;
+ private final SchemaFactory<ReviewDb> schema;
+
+ @Inject
+ SchemaUpgrade(@GerritServerConfig Config config, SchemaUpdater updater,
+ SchemaFactory<ReviewDb> schema) {
+ this.config = config;
+ this.updater = updater;
+ this.schema = schema;
+ }
+
+ void upgradeSchema() throws OrmException {
+ SchemaUpgradePolicy policy =
+ config.getEnum("site", null, "upgradeSchemaOnStartup",
+ SchemaUpgradePolicy.OFF);
+ if (policy == SchemaUpgradePolicy.AUTO
+ || policy == SchemaUpgradePolicy.AUTO_NO_PRUNE) {
+ final List<String> pruneList = new ArrayList<String>();
+ updater.update(new UpdateUI() {
+ @Override
+ public void message(String msg) {
+ log.info(msg);
+ }
+
+ @Override
+ public boolean yesno(boolean def, String msg) {
+ return true;
+ }
+
+ @Override
+ public boolean isBatch() {
+ return true;
+ }
+
+ @Override
+ public void pruneSchema(StatementExecutor e, List<String> prune) {
+ for (String p : prune) {
+ if (!pruneList.contains(p)) {
+ pruneList.add(p);
+ }
+ }
+ }
+ });
+
+ if (!pruneList.isEmpty() && policy == SchemaUpgradePolicy.AUTO) {
+ log.info("Pruning: " + pruneList.toString());
+ final JdbcSchema db = (JdbcSchema) schema.open();
+ try {
+ final JdbcExecutor e = new JdbcExecutor(db);
+ try {
+ for (String sql : pruneList) {
+ e.execute(sql);
+ }
+ } finally {
+ e.close();
+ }
+ } finally {
+ db.close();
+ }
+ }
+ }
+ }
+ }
+
+
private String myVersion() {
return com.google.gerrit.common.Version.getVersion();
}
@@ -204,10 +292,11 @@ public class Daemon extends SiteProgram {
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
- modules.add(new EhcachePoolImpl.Module());
+ modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
- modules.add(new PushReplication.Module());
+ modules.add(new SignedTokenRestTokenVerifier.Module());
+ modules.add(new PluginModule());
if (httpd) {
modules.add(new CanonicalWebUrlModule() {
@Override
@@ -231,13 +320,15 @@ public class Daemon extends SiteProgram {
private void initSshd() {
sshInjector = createSshInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setSshInjector(sshInjector);
manager.add(sshInjector);
}
private Injector createSshInjector() {
final List<Module> modules = new ArrayList<Module>();
if (sshd) {
- modules.add(new SshModule());
+ modules.add(sysInjector.getInstance(SshModule.class));
if (slave) {
modules.add(new SlaveCommandModule());
} else {
@@ -252,6 +343,9 @@ public class Daemon extends SiteProgram {
private void initHttpd() {
webInjector = createWebInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setHttpInjector(webInjector);
+
sysInjector.getInstance(HttpCanonicalWebUrlProvider.class)
.setHttpServletRequest(
webInjector.getProvider(HttpServletRequest.class));
@@ -262,19 +356,25 @@ public class Daemon extends SiteProgram {
private Injector createWebInjector() {
final List<Module> modules = new ArrayList<Module>();
+ if (sshd) {
+ modules.add(new ProjectQoSFilter.Module());
+ }
+ modules.add(RequestContextFilter.module());
+ modules.add(AllRequestFilter.module());
modules.add(CacheBasedWebSession.module());
modules.add(HttpContactStoreConnection.module());
modules.add(sysInjector.getInstance(GitOverHttpModule.class));
modules.add(sysInjector.getInstance(WebModule.class));
+ modules.add(new HttpPluginModule());
if (sshd) {
modules.add(sshInjector.getInstance(WebSshGlueModule.class));
- modules.add(new ProjectQoSFilter.Module());
} else {
modules.add(new NoSshModule());
}
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
- if (authConfig.getAuthType() == AuthType.OPENID) {
+ if (authConfig.getAuthType() == AuthType.OPENID ||
+ authConfig.getAuthType() == AuthType.OPENID_SSO) {
modules.add(new OpenIdModule());
}
modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 5f0bc8098a..525360d9db 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -17,17 +17,15 @@ package com.google.gerrit.pgm;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.pgm.util.SiteProgram;
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.server.ReviewDb;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.GroupCacheImpl;
-import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.ApprovalTypesProvider;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
@@ -36,6 +34,7 @@ import com.google.gerrit.server.git.CodeReviewNoteCreationException;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.NotesBranchUtil;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -45,7 +44,6 @@ import com.google.inject.Injector;
import com.google.inject.Scopes;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
@@ -100,11 +98,12 @@ public class ExportReviewNotes extends SiteProgram {
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
- install(new EhcachePoolImpl.Module());
+ install(new DefaultCacheFactory.Module());
install(new FactoryModule() {
@Override
protected void configure() {
factory(CreateCodeReviewNotes.Factory.class);
+ factory(NotesBranchUtil.Factory.class);
}
});
install(new LifecycleModule() {
@@ -173,21 +172,8 @@ public class ExportReviewNotes extends SiteProgram {
}
try {
CreateCodeReviewNotes notes = codeReviewNotesFactory.create(db, git);
- try {
- notes.loadBase();
- for (Change change : changes) {
- monitor.update(1);
- PatchSet ps = db.patchSets().get(change.currentPatchSetId());
- if (ps == null) {
- continue;
- }
- notes.add(change, ObjectId.fromString(ps.getRevision().get()));
- }
- notes.commit("Exported prior reviews from Gerrit Code Review\n");
- notes.updateRef();
- } finally {
- notes.release();
- }
+ notes.create(changes, null,
+ "Exported prior reviews from Gerrit Code Review\n", monitor);
} finally {
git.close();
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
index d967969bbf..a5ce9081c4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -47,6 +47,7 @@ public class Gsql extends SiteProgram {
manager.add(dbInjector);
manager.start();
RuntimeShutdown.add(new Runnable() {
+ @Override
public void run() {
try {
System.in.close();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index f06946fd87..95b8487ffd 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -141,12 +141,12 @@ public class Init extends SiteProgram {
}
final StringBuilder buf = new StringBuilder();
- buf.append(why.getMessage());
- why = why.getCause();
while (why != null) {
- buf.append("\n caused by ");
- buf.append(why.toString());
+ buf.append(why.getMessage());
why = why.getCause();
+ if (why != null) {
+ buf.append("\n caused by ");
+ }
}
throw die(buf.toString(), new RuntimeException("InitInjector failed", ce));
}
@@ -191,6 +191,11 @@ public class Init extends SiteProgram {
}
@Override
+ public boolean isBatch() {
+ return ui.isBatch();
+ }
+
+ @Override
public void pruneSchema(StatementExecutor e, List<String> prune) {
for (String p : prune) {
if (!pruneList.contains(p)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 451ed3012f..cabdc6432e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -43,7 +43,7 @@ public class Rulec extends SiteProgram {
@Option(name = "--all", usage = "recompile all rules")
private boolean all;
- @Option(name = "--quiet", usage = "supress some messsages")
+ @Option(name = "--quiet", usage = "suppress some messages")
private boolean quiet;
@Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index c09329a21b..795ba5befc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -33,7 +33,6 @@ import com.google.inject.Injector;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
@@ -109,7 +108,7 @@ public class ScanTrackingIds extends SiteProgram {
final Repository git;
try {
git = gitManager.openRepository(project);
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
return;
}
try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
new file mode 100644
index 0000000000..67f5c91f8c
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
@@ -0,0 +1,28 @@
+// 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.pgm;
+
+/** Policy for auto upgrading schema on server startup */
+public enum SchemaUpgradePolicy {
+
+ /** Perform schema migration if necessary and prune unused objects */
+ AUTO,
+
+ /** Like AUTO but don't prune unused objects */
+ AUTO_NO_PRUNE,
+
+ /** No automatic schema upgrade */
+ OFF
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 0a0a3cc6fd..582394009e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -17,8 +17,8 @@ package com.google.gerrit.pgm.http.jetty;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.lifecycle.LifecycleListener;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -38,8 +38,10 @@ import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.RequestLogHandler;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.FilterMapping;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
@@ -314,6 +316,11 @@ public class JettyServer {
final JettyEnv env) throws MalformedURLException, IOException {
final ServletContextHandler app = new ServletContextHandler();
+ // This enables the use of sessions in Jetty, feature available
+ // for Gerrit plug-ins to enable user-level sessions.
+ //
+ app.setSessionHandler(new SessionHandler());
+
// This is the path we are accessed by clients within our domain.
//
app.setContextPath(contextPath);
@@ -328,7 +335,8 @@ public class JettyServer {
// of using the listener to create the injector pass the one we
// already have built.
//
- app.addFilter(GuiceFilter.class, "/*", FilterMapping.DEFAULT);
+ GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
+ app.addFilter(new FilterHolder(filter), "/*", FilterMapping.DEFAULT);
app.addEventListener(new GuiceServletContextListener() {
@Override
protected Injector getInjector() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index ee7c794183..8d320d4cd8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -71,7 +71,7 @@ public class ProjectQoSFilter implements Filter {
private static final String CANCEL = ATT_SPACE + "/CANCEL";
private static final String FILTER_RE =
- "^/p/(.*)/(git-upload-pack|git-receive-pack)$";
+ "^/(.*)/(git-upload-pack|git-receive-pack)$";
private static final Pattern URI_PATTERN = Pattern.compile(FILTER_RE);
public static class Module extends ServletModule {
@@ -97,7 +97,7 @@ public class ProjectQoSFilter implements Filter {
this.userProvider = userProvider;
this.queue = queue;
this.context = context;
- this.maxWait = getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES);
+ this.maxWait = MINUTES.toMillis(getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES));
}
@Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index f809c73308..fa4dc142b3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -85,5 +85,9 @@ class InitAuth implements InitStep {
if (auth.getSecure("registerEmailPrivateKey") == null) {
auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
}
+
+ if (auth.getSecure("restTokenPrivateKey") == null) {
+ auth.setSecure("restTokenPrivateKey", SignedToken.generateRandomKey());
+ }
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index a55cea5124..8b3d87ea8c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -43,6 +43,7 @@ public class InitModule extends FactoryModule {
step().to(InitSshd.class);
step().to(InitHttpd.class);
step().to(InitCache.class);
+ step().to(InitPlugins.class);
}
protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
new file mode 100644
index 0000000000..155fe4c859
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -0,0 +1,140 @@
+// 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.pgm.init;
+
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+@Singleton
+public class InitPlugins implements InitStep {
+ private final static String PLUGIN_DIR = "WEB-INF/plugins/";
+ private final static String JAR = ".jar";
+
+ private final ConsoleUI ui;
+ private final SitePaths site;
+
+ @Inject
+ InitPlugins(final ConsoleUI ui, final SitePaths site) {
+ this.ui = ui;
+ this.site = site;
+ }
+
+ @Override
+ public void run() throws Exception {
+ ui.header("Plugins");
+
+ final File myWar;
+ try {
+ myWar = GerritLauncher.getDistributionArchive();
+ } catch (FileNotFoundException e) {
+ System.err.println("warn: Cannot find gerrit.war");
+ return;
+ }
+
+ boolean foundPlugin = false;
+ try {
+ final ZipFile zf = new ZipFile(myWar);
+ try {
+ final Enumeration<? extends ZipEntry> e = zf.entries();
+ while (e.hasMoreElements()) {
+ final ZipEntry ze = e.nextElement();
+ if (ze.isDirectory()) {
+ continue;
+ }
+
+ if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
+ if (!foundPlugin) {
+ if (!ui.yesno(false, "Prompt to install core plugins")) {
+ return;
+ }
+ foundPlugin = true;
+ }
+
+ final String pluginJarName = new File(ze.getName()).getName();
+ final String pluginName = pluginJarName.substring(0, pluginJarName.length() - JAR.length());
+
+ final InputStream in = zf.getInputStream(ze);
+ try {
+ final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+ final String pluginVersion = getVersion(tmpPlugin);
+
+ if (!ui.yesno(false, "Install plugin %s version %s", pluginName,
+ pluginVersion)) {
+ tmpPlugin.delete();
+ continue;
+ }
+
+ final File plugin = new File(site.plugins_dir, pluginJarName);
+ if (plugin.exists()) {
+ final String installedPluginVersion = getVersion(plugin);
+ if (!ui.yesno(false,
+ "version %s is already installed, overwrite it",
+ installedPluginVersion)) {
+ tmpPlugin.delete();
+ continue;
+ }
+ if (!plugin.delete()) {
+ throw new IOException("Failed to delete plugin " + pluginName
+ + ": " + plugin.getAbsolutePath());
+ }
+ }
+ if (!tmpPlugin.renameTo(plugin)) {
+ throw new IOException("Failed to install plugin " + pluginName
+ + ": " + tmpPlugin.getAbsolutePath() + " -> "
+ + plugin.getAbsolutePath());
+ }
+ } finally {
+ in.close();
+ }
+ }
+ }
+ } finally {
+ zf.close();
+ }
+ } catch (IOException e) {
+ throw new IOException("Failure during plugin installation", e);
+ }
+
+ if (!foundPlugin) {
+ ui.message("No plugins found.");
+ }
+ }
+
+ private static String getVersion(final File plugin) throws IOException {
+ final JarFile jarFile = new JarFile(plugin);
+ try {
+ final Manifest manifest = jarFile.getManifest();
+ final Attributes main = manifest.getMainAttributes();
+ return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ } finally {
+ jarFile.close();
+ }
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index dae08934ad..f8ef6370c5 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -67,9 +67,12 @@ public class SitePathInitializer {
mkdir(site.bin_dir);
mkdir(site.etc_dir);
mkdir(site.lib_dir);
+ mkdir(site.tmp_dir);
mkdir(site.logs_dir);
mkdir(site.mail_dir);
mkdir(site.static_dir);
+ mkdir(site.plugins_dir);
+ mkdir(site.data_dir);
for (InitStep step : steps) {
step.run();
@@ -78,12 +81,9 @@ public class SitePathInitializer {
savePublic(flags.cfg);
saveSecure(flags.sec);
- if (!site.replication_config.exists()) {
- site.replication_config.createNewFile();
- }
-
extract(site.gerrit_sh, Init.class, "gerrit.sh");
chmod(0755, site.gerrit_sh);
+ chmod(0700, site.tmp_dir);
extractMailExample("Abandoned.vm");
extractMailExample("ChangeFooter.vm");
@@ -92,8 +92,11 @@ public class SitePathInitializer {
extractMailExample("Merged.vm");
extractMailExample("MergeFail.vm");
extractMailExample("NewChange.vm");
+ extractMailExample("RebasedPatchSet.vm");
extractMailExample("RegisterNewEmail.vm");
extractMailExample("ReplacePatchSet.vm");
+ extractMailExample("Restored.vm");
+ extractMailExample("Reverted.vm");
if (!ui.isBatch()) {
System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
index 1e93651646..b4e0fad61c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
@@ -61,6 +61,9 @@ public abstract class ConsoleUI {
/** Display a header message before a series of prompts. */
public abstract void header(String fmt, Object... args);
+ /** Display a message. */
+ public abstract void message(String fmt, Object... args);
+
/** Request the user to answer a yes/no question. */
public abstract boolean yesno(Boolean def, String fmt, Object... args);
@@ -215,6 +218,11 @@ public abstract class ConsoleUI {
fmt = fmt.replaceAll("\n", "\n*** ");
console.printf("\n*** " + fmt + "\n*** \n\n", args);
}
+
+ @Override
+ public void message(String fmt, Object... args) {
+ console.printf(fmt, args);
+ }
}
private static class Batch extends ConsoleUI {
@@ -250,5 +258,9 @@ public abstract class ConsoleUI {
@Override
public void header(String fmt, Object... args) {
}
+
+ @Override
+ public void message(String fmt, Object... args) {
+ }
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 14c8d9f67e..68762bbc3e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,7 +14,7 @@
package com.google.gerrit.pgm.util;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.SitePaths;
import org.apache.log4j.Appender;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 23f36a1dc2..57cc7c4310 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -16,7 +16,7 @@ package com.google.gerrit.pgm.util;
import static java.util.concurrent.TimeUnit.HOURS;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.WorkQueue;
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 414884752c..3857ebd075 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -176,6 +176,8 @@ test -r "$GERRIT_CONFIG" || {
GERRIT_PID="$GERRIT_SITE/logs/gerrit.pid"
GERRIT_RUN="$GERRIT_SITE/logs/gerrit.run"
+GERRIT_TMP="$GERRIT_SITE/tmp"
+export GERRIT_TMP
##################################################
# Check for JAVA_HOME
@@ -302,7 +304,7 @@ if test -z "$GERRIT_WAR" -a -n "$GERRIT_USER" ; then
done
fi
if test -z "$GERRIT_WAR" ; then
- echo >&2 "** ERROR: Cannot find gerrit.war (try setting gerrit.war)"
+ echo >&2 "** ERROR: Cannot find gerrit.war (try setting \$GERRIT_WAR)"
exit 1
fi
@@ -492,6 +494,7 @@ case "$ACTION" in
echo " GERRIT_SITE = $GERRIT_SITE"
echo " GERRIT_CONFIG = $GERRIT_CONFIG"
echo " GERRIT_PID = $GERRIT_PID"
+ echo " GERRIT_TMP = $GERRIT_TMP"
echo " GERRIT_WAR = $GERRIT_WAR"
echo " GERRIT_FDS = $GERRIT_FDS"
echo " GERRIT_USER = $GERRIT_USER"
diff --git a/gerrit-plugin-api/.gitignore b/gerrit-plugin-api/.gitignore
new file mode 100644
index 0000000000..e87ebb8f6e
--- /dev/null
+++ b/gerrit-plugin-api/.gitignore
@@ -0,0 +1,8 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.core.resources.prefs
+/.settings/org.eclipse.jdt.core.prefs
+/gerrit-plugin-api.iml
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
new file mode 100644
index 0000000000..84f6f7b242
--- /dev/null
+++ b/gerrit-plugin-api/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.5-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>gerrit-plugin-api</artifactId>
+ <name>Gerrit Code Review - Plugin API</name>
+
+ <description>
+ API for tightly coupled plugins to compile against
+ </description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-sshd</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-httpd</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <configuration>
+ <createSourcesJar>true</createSourcesJar>
+ <artifactSet>
+ <excludes>
+ <exclude>gwtexpui:gwtexpui</exclude>
+ <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
+ <exclude>com.google.gerrit:gerrit-prettify</exclude>
+ <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
+ <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
+ <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
+ <exclude>com.google.gerrit:juniversalchardet</exclude>
+
+ <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
+ <exclude>org.slf4j:slf4j-log4j12</exclude>
+ <exclude>log4j:log4j</exclude>
+
+ <exclude>commons-collections:commons-collections</exclude>
+ <exclude>commons-codec:commons-codec</exclude>
+ <exclude>commons-dbcp:commons-dbcp</exclude>
+ <exclude>commons-lang:commons-lang</exclude>
+ <exclude>commons-net:commons-net</exclude>
+ <exclude>commons-pool:commons-pool</exclude>
+
+ <exclude>asm:asm</exclude>
+ <exclude>eu.medsea.mimeutil:mime-util</exclude>
+ <exclude>org.antlr:antlr</exclude>
+ <exclude>org.antlr:antlr-runtime</exclude>
+ <exclude>org.apache.mina:mina-core</exclude>
+ <exclude>oro:oro</exclude>
+ </excludes>
+ </artifactSet>
+ <filters>
+ <filter>
+ <artifact>com.google.gerrit:gerrit-server</artifact>
+ <excludes>
+ <exclude>gerrit/**</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-plugin-archetype/.gitignore
index 20251d4700..80d62575a1 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/.gitignore
@@ -1,5 +1,5 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000000..abdea9ac03
--- /dev/null
+++ b/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000000..470942d4f6
--- /dev/null
+++ b/gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+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_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_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_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.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.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.format_guardian_clause_on_one_line=false
+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_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=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_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_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_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_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_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_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.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_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
new file mode 100644
index 0000000000..dd1794b8ce
--- /dev/null
+++ b/gerrit-plugin-archetype/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.5-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>gerrit-plugin-archetype</artifactId>
+ <name>Gerrit Code Review - Plugin Archetype</name>
+
+ <properties>
+ <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
+ </properties>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ <includes>
+ <include>META-INF/maven/archetype-metadata.xml</include>
+ </includes>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>false</filtering>
+ <excludes>
+ <exclude>META-INF/maven/archetype-metadata.xml</exclude>
+ </excludes>
+ </resource>
+ </resources>
+ </build>
+
+</project>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000000..ce8fa1aae6
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<archetype-descriptor name="Gerrit Plugin">
+ <requiredProperties>
+ <requiredProperty key="pluginName"/>
+
+ <requiredProperty key="Gerrit-Module">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="Gerrit-SshModule">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="Gerrit-HttpModule">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+
+ <requiredProperty key="Implementation-Vendor"/>
+ <requiredProperty key="Implementation-Url"/>
+
+ <requiredProperty key="gerritApiType">
+ <defaultValue>plugin</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="gerritApiVersion">
+ <defaultValue>${defaultGerritApiVersion}</defaultValue>
+ </requiredProperty>
+ </requiredProperties>
+
+ <fileSets>
+ <fileSet filtered="true" packaged="true">
+ <directory>src/main/java</directory>
+ <includes>
+ <include>**/*.java</include>
+ </includes>
+ </fileSet>
+
+ <fileSet filtered="true">
+ <directory>src/main/resources/Documentation</directory>
+ <includes>
+ <include>**/*.md</include>
+ </includes>
+ </fileSet>
+
+ <fileSet>
+ <directory></directory>
+ <includes>
+ <include>.gitignore</include>
+ <include>LICENSE</include>
+ </includes>
+ </fileSet>
+ </fileSets>
+</archetype-descriptor>
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
new file mode 100644
index 0000000000..80d62575a1
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
@@ -0,0 +1,5 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
new file mode 100644
index 0000000000..11069edd79
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
@@ -0,0 +1,201 @@
+ 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-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000000..92099faa77
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,103 @@
+<!--
+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.
+-->
+<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>
+
+ <groupId>${groupId}</groupId>
+ <artifactId>${artifactId}</artifactId>
+ <packaging>jar</packaging>
+ <version>${version}</version>
+ <name>${pluginName}</name>
+
+ <properties>
+ <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <archive>
+ <manifestEntries>
+#if ($Gerrit-Module.equalsIgnoreCase("Y"))
+ <Gerrit-Module>${package}.Module</Gerrit-Module>
+#end
+#if ($Gerrit-SshModule.equalsIgnoreCase("Y"))
+ <Gerrit-SshModule>${package}.SshModule</Gerrit-SshModule>
+#end
+#if ($Gerrit-HttpModule.equalsIgnoreCase("Y"))
+ <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule>
+#end
+
+ <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+ <Implementation-URL>${Implementation-Url}</Implementation-URL>
+
+ <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+
+ <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+ <version>${Gerrit-ApiVersion}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <repositories>
+ <repository>
+ <id>gerrit-api-repository</id>
+#if ($gerritApiVersion.endsWith("SNAPSHOT"))
+ <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+#else
+ <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+#end
+ </repository>
+ </repositories>
+</project>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
index 43039e1475..2840112578 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package ${package};
+import com.google.inject.servlet.ServletModule;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+class HttpModule extends ServletModule {
+ @Override
+ protected void configureServlets() {
+ // TODO
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index 3370b08b1a..0d283497cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package ${package};
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.inject.AbstractModule;
+
+class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ // TODO
+ }
}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
new file mode 100644
index 0000000000..aa15ca561e
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
@@ -0,0 +1,24 @@
+// 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 ${package};
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+class SshModule extends PluginCommandModule {
+ @Override
+ protected void configureCommands() {
+ // command("my-command").to(MyCommand.class);
+ }
+}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
new file mode 100644
index 0000000000..beecb9036e
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
@@ -0,0 +1 @@
+TODO: command documentation
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000000..bde3084487
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
@@ -0,0 +1 @@
+TODO: config documentation
diff --git a/gerrit-prettify/.gitignore b/gerrit-prettify/.gitignore
index 194bedcbc4..8cf95ef452 100644
--- a/gerrit-prettify/.gitignore
+++ b/gerrit-prettify/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-prettify.iml \ No newline at end of file
diff --git a/gerrit-prettify/.settings/org.eclipse.core.resources.prefs b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
index e7d66802f5..abdea9ac03 100644
--- a/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index f5bd3d636b..9354274882 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-prettify</artifactId>
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/common/PrettifyConstants.java
index e14063a1d1..df603058bb 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
@@ -23,4 +23,5 @@ public interface PrettifyConstants extends Constants {
String wseTabAfterSpace();
String wseTrailingSpace();
String wseBareCR();
+ String leCR();
}
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/common/PrettifyConstants.properties
index d440c65c55..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/common/PrettifyConstants.properties
@@ -1,3 +1,4 @@
wseTabAfterSpace=Whitespace error: Tab after space
wseTrailingSpace=Whitespace error: Trailing space at end of line
-wseBareCR=Whitespace error: CR without LF
+wseBareCR=CR without LF
+leCR=Carriage Return
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/common/PrettyFormatter.java
index c9a9ab8bd8..151149b52e 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -335,6 +335,10 @@ public abstract class PrettyFormatter implements SparseHtmlFile {
html = showTrailingWhitespace(html);
}
+ if (diffPrefs.isShowLineEndings()){
+ html = showLineEndings(html);
+ }
+
if (diffPrefs.isShowTabs()) {
String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
@@ -449,12 +453,11 @@ public abstract class PrettyFormatter implements SparseHtmlFile {
} else if (end) {
if (cr == src.length() - 1) {
- buf.append(src.substring(0, cr));
+ buf.append(src.substring(0, cr + 1));
return;
}
} else if (cr == src.length() - 2 && src.charAt(cr + 1) == '\n') {
- buf.append(src.substring(0, cr));
- buf.append('\n');
+ buf.append(src);
return;
}
@@ -499,6 +502,14 @@ public abstract class PrettyFormatter implements SparseHtmlFile {
return src;
}
+ private SafeHtml showLineEndings(SafeHtml src) {
+ final String r = "<span class=\"lecr\""
+ + " title=\"" + PrettifyConstants.C.leCR() + "\"" //
+ + ">\\\\r</span>";
+ src = src.replaceAll("\r", r);
+ return src;
+ }
+
private String expandTabs(String html) {
StringBuilder tmp = new StringBuilder();
int i = 0;
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 a5373b8fb9..1a5468caa7 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
@@ -249,7 +249,7 @@ public class SparseFileContent {
range.lines = lines;
SparseFileContent r = new SparseFileContent();
- r.setSize(size());
+ r.setSize(lines.size());
r.setMissingNewlineAtEnd(isMissingNewlineAtEnd());
r.setPath(getPath());
r.ranges.add(range);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
index 3478e68763..23e7e4679e 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
@@ -14,6 +14,7 @@
*/
@external .wse;
+@external .lecr;
@external .vt;
@external .wdd;
@external .wdi;
@@ -35,6 +36,19 @@
cursor: pointer;
}
+.lecr {
+ border-bottom: #aaaaaa 1px dashed;
+ border-left: #aaaaaa 1px dashed;
+ padding-bottom: 0px;
+ margin: 0px 2px;
+ padding-left: 2px;
+ padding-right: 2px;
+ border-top: #aaaaaa 1px dashed;
+ border-right: #aaaaaa 1px dashed;
+ padding-top: 0px;
+ cursor: pointer;
+}
+
.vt,
.vt .str,
.vt .kwd,
diff --git a/gerrit-reviewdb/.gitignore b/gerrit-reviewdb/.gitignore
index 194bedcbc4..812ddd087f 100644
--- a/gerrit-reviewdb/.gitignore
+++ b/gerrit-reviewdb/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-reviewdb.iml \ No newline at end of file
diff --git a/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
index e7d66802f5..abdea9ac03 100644
--- a/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 24d6a1b088..f9fb49eec7 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
deleted file mode 100644
index 0ed7410448..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// 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.gerrit.reviewdb.client;
-
-import java.sql.Timestamp;
-
-/** Base for {@link AccountAgreement} or {@link AccountGroupAgreement}. */
-public interface AbstractAgreement {
- public static enum Status {
- NEW('n'),
-
- VERIFIED('V'),
-
- REJECTED('R');
-
- private final char code;
-
- private Status(final char c) {
- code = c;
- }
-
- public char getCode() {
- return code;
- }
-
- public static Status forCode(final char c) {
- for (final Status s : Status.values()) {
- if (s.code == c) {
- return s;
- }
- }
- return null;
- }
- }
-
- public ContributorAgreement.Id getAgreementId();
-
- public Timestamp getAcceptedOn();
-
- public Status getStatus();
-
- public Timestamp getReviewedOn();
-
- public Account.Id getReviewedBy();
-
- public String getReviewComments();
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
deleted file mode 100644
index baa9b5cee9..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
+++ /dev/null
@@ -1,122 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Electronic acceptance of a {@link ContributorAgreement} by {@link Account} */
-public final class AccountAgreement implements AbstractAgreement {
- public static class Key extends CompoundKey<Account.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected Account.Id accountId;
-
- @Column(id = 2)
- protected ContributorAgreement.Id claId;
-
- protected Key() {
- accountId = new Account.Id();
- claId = new ContributorAgreement.Id();
- }
-
- public Key(final Account.Id account, final ContributorAgreement.Id cla) {
- this.accountId = account;
- this.claId = cla;
- }
-
- @Override
- public Account.Id getParentKey() {
- return accountId;
- }
-
- public ContributorAgreement.Id getContributorAgreementId() {
- return claId;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {claId};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- @Column(id = 2)
- protected Timestamp acceptedOn;
-
- @Column(id = 3)
- protected char status;
-
- @Column(id = 4, notNull = false)
- protected Account.Id reviewedBy;
-
- @Column(id = 5, notNull = false)
- protected Timestamp reviewedOn;
-
- @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
- protected String reviewComments;
-
- protected AccountAgreement() {
- }
-
- public AccountAgreement(final AccountAgreement.Key k) {
- key = k;
- acceptedOn = new Timestamp(System.currentTimeMillis());
- status = Status.NEW.getCode();
- }
-
- public AccountAgreement.Key getKey() {
- return key;
- }
-
- public ContributorAgreement.Id getAgreementId() {
- return key.claId;
- }
-
- public Timestamp getAcceptedOn() {
- return acceptedOn;
- }
-
- public Status getStatus() {
- return Status.forCode(status);
- }
-
- public Timestamp getReviewedOn() {
- return reviewedOn;
- }
-
- public Account.Id getReviewedBy() {
- return reviewedBy;
- }
-
- public String getReviewComments() {
- return reviewComments;
- }
-
- public void setReviewComments(final String s) {
- reviewComments = s;
- }
-
- public void review(final Status newStatus, final Account.Id by) {
- status = newStatus.getCode();
- reviewedBy = by;
- reviewedOn = new Timestamp(System.currentTimeMillis());
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index 3b04725f73..afafd460f4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -62,6 +62,7 @@ public class AccountDiffPreference {
p.setLineLength(100);
p.setSyntaxHighlighting(true);
p.setShowWhitespaceErrors(true);
+ p.setShowLineEndings(true);
p.setIntralineDifference(true);
p.setShowTabs(true);
p.setContext(DEFAULT_CONTEXT);
@@ -112,6 +113,9 @@ public class AccountDiffPreference {
@Column(id = 14)
protected boolean manualReview;
+ @Column(id = 15)
+ protected boolean showLineEndings;
+
protected AccountDiffPreference() {
}
@@ -126,6 +130,7 @@ public class AccountDiffPreference {
this.lineLength = p.lineLength;
this.syntaxHighlighting = p.syntaxHighlighting;
this.showWhitespaceErrors = p.showWhitespaceErrors;
+ this.showLineEndings = p.showLineEndings;
this.intralineDifference = p.intralineDifference;
this.showTabs = p.showTabs;
this.skipDeleted = p.skipDeleted;
@@ -180,6 +185,14 @@ public class AccountDiffPreference {
this.showWhitespaceErrors = showWhitespaceErrors;
}
+ public boolean isShowLineEndings() {
+ return showLineEndings;
+ }
+
+ public void setShowLineEndings(boolean showLineEndings) {
+ this.showLineEndings = showLineEndings;
+ }
+
public boolean isIntralineDifference() {
return intralineDifference;
}
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 b9ff4fb095..6f121ee03a 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
@@ -32,7 +32,7 @@ public final class AccountGeneralPreferences {
/** Preferred method to download a change. */
public static enum DownloadCommand {
- REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH;
+ REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH, DEFAULT_DOWNLOADS;
}
public static enum DateFormat {
@@ -118,10 +118,10 @@ public final class AccountGeneralPreferences {
* (show latest patch set on top).
*/
@Column(id = 10)
- protected boolean displayPatchSetsInReverseOrder;
+ protected boolean reversePatchSetOrder;
@Column(id = 11)
- protected boolean displayPersonNameInReviewCategory;
+ protected boolean showUsernameInReviewCategory;
public AccountGeneralPreferences() {
}
@@ -188,20 +188,20 @@ public final class AccountGeneralPreferences {
copySelfOnEmail = includeSelfOnEmail;
}
- public boolean isDisplayPatchSetsInReverseOrder() {
- return displayPatchSetsInReverseOrder;
+ public boolean isReversePatchSetOrder() {
+ return reversePatchSetOrder;
}
- public void setDisplayPatchSetsInReverseOrder(final boolean displayPatchSetsInReverseOrder) {
- this.displayPatchSetsInReverseOrder = displayPatchSetsInReverseOrder;
+ public void setReversePatchSetOrder(final boolean reversePatchSetOrder) {
+ this.reversePatchSetOrder = reversePatchSetOrder;
}
- public boolean isDisplayPersonNameInReviewCategory() {
- return displayPersonNameInReviewCategory;
+ public boolean isShowUsernameInReviewCategory() {
+ return showUsernameInReviewCategory;
}
- public void setDisplayPersonNameInReviewCategory(final boolean displayPersonNameInReviewCategory) {
- this.displayPersonNameInReviewCategory = displayPersonNameInReviewCategory;
+ public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
+ this.showUsernameInReviewCategory = showUsernameInReviewCategory;
}
public DateFormat getDateFormat() {
@@ -231,8 +231,8 @@ public final class AccountGeneralPreferences {
showSiteHeader = true;
useFlashClipboard = true;
copySelfOnEmail = false;
- displayPatchSetsInReverseOrder = false;
- displayPersonNameInReviewCategory = false;
+ reversePatchSetOrder = false;
+ showUsernameInReviewCategory = false;
downloadUrl = null;
downloadCommand = null;
dateFormat = null;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 8e2541ab44..061ef3e42e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -51,7 +51,7 @@ public final class AccountGroup {
StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
- @Column(id = 1, length = 40)
+ @Column(id = 1)
protected String uuid;
protected UUID() {
@@ -79,30 +79,10 @@ public final class AccountGroup {
}
}
- /** Distinguished name, within organization directory server. */
- public static class ExternalNameKey extends
- StringKey<com.google.gwtorm.client.Key<?>> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected String name;
-
- protected ExternalNameKey() {
- }
-
- public ExternalNameKey(final String n) {
- name = n;
- }
-
- @Override
- public String get() {
- return name;
- }
-
- @Override
- protected void set(String newValue) {
- name = newValue;
- }
+ /** @return true if the UUID is for a group managed within Gerrit. */
+ public static boolean isInternalGroup(AccountGroup.UUID uuid) {
+ return uuid.get().startsWith("global:")
+ || uuid.get().matches("^[0-9a-f]{40}$");
}
/** Synthetic key to link to within the database */
@@ -157,20 +137,7 @@ public final class AccountGroup {
* who is a member of the owner group. These groups are not treated special
* in the code.
*/
- INTERNAL,
-
- /**
- * Group defined by external LDAP database.
- * <p>
- * A group whose membership is determined by the LDAP directory that we
- * connect to for user and group information. In UI contexts the membership
- * of the group is not displayed, as it may be exceedingly large, or might
- * contain users who have never logged into this server before (and thus
- * have no matching account record). Adding or removing users from an LDAP
- * group requires making edits through the LDAP directory, and cannot be
- * done through our UI.
- */
- LDAP;
+ INTERNAL;
}
/** Common UUID assigned to the "Project Owners" placeholder group. */
@@ -193,14 +160,6 @@ public final class AccountGroup {
@Column(id = 2)
protected Id groupId;
- /**
- * Identity of the group whose members can manage this group.
- * <p>
- * This can be a self-reference to indicate the group's members manage itself.
- */
- @Column(id = 3)
- protected Id ownerGroupId;
-
/** A textual description of the group's purpose. */
@Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
protected String description;
@@ -209,10 +168,6 @@ public final class AccountGroup {
@Column(id = 5, length = 8)
protected String groupType;
- /** Distinguished name in the directory server. */
- @Column(id = 6, notNull = false)
- protected ExternalNameKey externalName;
-
@Column(id = 7)
protected boolean visibleToAll;
@@ -220,6 +175,14 @@ public final class AccountGroup {
@Column(id = 9)
protected UUID groupUUID;
+ /**
+ * Identity of the group whose members can manage this group.
+ * <p>
+ * This can be a self-reference to indicate the group's members manage itself.
+ */
+ @Column(id = 10)
+ protected UUID ownerGroupUUID;
+
protected AccountGroup() {
}
@@ -227,9 +190,9 @@ public final class AccountGroup {
final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
name = newName;
groupId = newId;
- ownerGroupId = groupId;
visibleToAll = false;
groupUUID = uuid;
+ ownerGroupUUID = groupUUID;
setType(Type.INTERNAL);
}
@@ -257,12 +220,12 @@ public final class AccountGroup {
description = d;
}
- public AccountGroup.Id getOwnerGroupId() {
- return ownerGroupId;
+ public AccountGroup.UUID getOwnerGroupUUID() {
+ return ownerGroupUUID;
}
- public void setOwnerGroupId(final AccountGroup.Id id) {
- ownerGroupId = id;
+ public void setOwnerGroupUUID(final AccountGroup.UUID uuid) {
+ ownerGroupUUID = uuid;
}
public Type getType() {
@@ -273,14 +236,6 @@ public final class AccountGroup {
groupType = t.name();
}
- public ExternalNameKey getExternalNameKey() {
- return externalName;
- }
-
- public void setExternalNameKey(final ExternalNameKey k) {
- externalName = k;
- }
-
public void setVisibleToAll(final boolean visibleToAll) {
this.visibleToAll = visibleToAll;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
deleted file mode 100644
index c712b3db8c..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// 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.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/**
- * Acceptance of a {@link ContributorAgreement} by an {@link AccountGroup}.
- */
-public final class AccountGroupAgreement implements AbstractAgreement {
- public static class Key extends CompoundKey<AccountGroup.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected AccountGroup.Id groupId;
-
- @Column(id = 2)
- protected ContributorAgreement.Id claId;
-
- protected Key() {
- groupId = new AccountGroup.Id();
- claId = new ContributorAgreement.Id();
- }
-
- public Key(final AccountGroup.Id group, final ContributorAgreement.Id cla) {
- this.groupId = group;
- this.claId = cla;
- }
-
- @Override
- public AccountGroup.Id getParentKey() {
- return groupId;
- }
-
- public ContributorAgreement.Id getContributorAgreementId() {
- return claId;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {claId};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- @Column(id = 2)
- protected Timestamp acceptedOn;
-
- @Column(id = 3)
- protected char status;
-
- @Column(id = 4, notNull = false)
- protected Account.Id reviewedBy;
-
- @Column(id = 5, notNull = false)
- protected Timestamp reviewedOn;
-
- @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
- protected String reviewComments;
-
- protected AccountGroupAgreement() {
- }
-
- public AccountGroupAgreement(final AccountGroupAgreement.Key k) {
- key = k;
- acceptedOn = new Timestamp(System.currentTimeMillis());
- status = Status.NEW.getCode();
- }
-
- public AccountGroupAgreement.Key getKey() {
- return key;
- }
-
- public ContributorAgreement.Id getAgreementId() {
- return key.claId;
- }
-
- public Timestamp getAcceptedOn() {
- return acceptedOn;
- }
-
- public Status getStatus() {
- return Status.forCode(status);
- }
-
- public Timestamp getReviewedOn() {
- return reviewedOn;
- }
-
- public Account.Id getReviewedBy() {
- return reviewedBy;
- }
-
- public String getReviewComments() {
- return reviewComments;
- }
-
- public void setReviewComments(final String s) {
- reviewComments = s;
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 8ba891234c..523134b904 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -80,9 +80,14 @@ public final class AccountGroupMemberAudit {
public AccountGroupMemberAudit(final AccountGroupMember m,
final Account.Id adder) {
+ this(m, adder, now());
+ }
+
+ public AccountGroupMemberAudit(final AccountGroupMember m,
+ final Account.Id adder, Timestamp addedOn) {
final Account.Id who = m.getAccountId();
final AccountGroup.Id group = m.getAccountGroupId();
- key = new AccountGroupMemberAudit.Key(who, group, now());
+ key = new AccountGroupMemberAudit.Key(who, group, addedOn);
addedBy = adder;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index 93e6fb322d..e592101b21 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,7 @@ import com.google.gwtorm.client.StringKey;
public final class AccountProjectWatch {
public enum NotifyType {
- NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
+ NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
}
public static final String FILTER_ALL = "*";
@@ -159,6 +159,12 @@ public final class AccountProjectWatch {
case SUBMITTED_CHANGES:
notifySubmittedChanges = v;
break;
+
+ case ALL:
+ notifyNewChanges = v;
+ notifyAllComments = v;
+ notifySubmittedChanges = v;
+ break;
}
}
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
index bb252654f1..7d61975926 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
@@ -105,7 +105,8 @@ public final class ApprovalCategory {
char c = name.charAt(i);
if (('0' <= c && c <= '9') //
|| ('a' <= c && c <= 'z') //
- || ('A' <= c && c <= 'Z')) {
+ || ('A' <= c && c <= 'Z') //
+ || (c == '-')) {
r.append(c);
} else if (c == ' ') {
r.append('-');
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
index 962426b65a..b615fc574f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
@@ -18,6 +18,9 @@ public enum AuthType {
/** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
OPENID,
+ /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */
+ OPENID_SSO,
+
/**
* Login relies upon the container/web server security.
* <p>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 61e6a97e92..bfbacc0adb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -491,6 +491,10 @@ public final class Change {
--nbrPatchSets;
}
+ public void updateNumberOfPatchSets(int max) {
+ nbrPatchSets = Math.max(nbrPatchSets, max);
+ }
+
public PatchSet.Id currPatchSetId() {
return new PatchSet.Id(changeId, nbrPatchSets);
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
deleted file mode 100644
index 12e7459280..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
+++ /dev/null
@@ -1,140 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-
-/**
- * An agreement {@link Account} must acknowledge to contribute changes.
- *
- * @see AccountAgreement
- */
-public final class ContributorAgreement {
- public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1, name = "cla_id")
- protected int id;
-
- protected Id() {
- }
-
- public Id(final int id) {
- this.id = id;
- }
-
- @Override
- public int get() {
- return id;
- }
-
- @Override
- protected void set(int newValue) {
- id = newValue;
- }
- }
-
- @Column(id = 1)
- protected Id id;
-
- /** Is this an active agreement contributors can use. */
- @Column(id = 2)
- protected boolean active;
-
- /** Does this agreement require the {@link Account} to have contact details? */
- @Column(id = 3)
- protected boolean requireContactInformation;
-
- /** Does this agreement automatically verify new accounts? */
- @Column(id = 4)
- protected boolean autoVerify;
-
- /** A short name for the agreement. */
- @Column(id = 5, length = 40)
- protected String shortName;
-
- /** A short one-line description text to appear next to the name. */
- @Column(id = 6, notNull = false)
- protected String shortDescription;
-
- /** Web address of the agreement documentation. */
- @Column(id = 7)
- protected String agreementUrl;
-
- protected ContributorAgreement() {
- }
-
- /**
- * Create a new agreement.
- *
- * @param newId unique id, see
- * {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
- * @param name a short title/name for the agreement.
- */
- public ContributorAgreement(final ContributorAgreement.Id newId,
- final String name) {
- id = newId;
- shortName = name;
- }
-
- public ContributorAgreement.Id getId() {
- return id;
- }
-
- public boolean isActive() {
- return active;
- }
-
- public void setActive(final boolean a) {
- active = a;
- }
-
- public boolean isAutoVerify() {
- return autoVerify;
- }
-
- public void setAutoVerify(final boolean g) {
- autoVerify = g;
- }
-
- public boolean isRequireContactInformation() {
- return requireContactInformation;
- }
-
- public void setRequireContactInformation(final boolean r) {
- requireContactInformation = r;
- }
-
- public String getShortName() {
- return shortName;
- }
-
- public String getShortDescription() {
- return shortDescription;
- }
-
- public void setShortDescription(final String d) {
- shortDescription = d;
- }
-
- public String getAgreementUrl() {
- return agreementUrl;
- }
-
- public void setAgreementUrl(final String h) {
- agreementUrl = h;
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 0d0ea88d22..00e282b520 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -82,7 +82,10 @@ public final class Patch {
RENAMED('R'),
/** Path was copied from {@link Patch#getSourceFileName()}. */
- COPIED('C');
+ COPIED('C'),
+
+ /** Sufficient amount of content changed to claim the file was written. */
+ REWRITE('W');
private final char code;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
index e73dd73558..c664285b77 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
@@ -20,7 +20,7 @@ import com.google.gwtorm.client.StringKey;
/** External tracking id associated with a {@link Change} */
public final class TrackingId {
- public static final int TRACKING_ID_MAX_CHAR = 20;
+ public static final int TRACKING_ID_MAX_CHAR = 32;
public static final int TRACKING_SYSTEM_MAX_CHAR = 10;
/** External tracking id */
@@ -80,20 +80,20 @@ public final class TrackingId {
protected Change.Id changeId;
@Column(id = 2)
- protected Id trackingId;
+ protected Id trackingKey;
@Column(id = 3)
protected System trackingSystem;
protected Key() {
changeId = new Change.Id();
- trackingId = new Id();
+ trackingKey = new Id();
trackingSystem = new System();
}
protected Key(final Change.Id ch, final Id id, final System s) {
changeId = ch;
- trackingId = id;
+ trackingKey = id;
trackingSystem = s;
}
@@ -103,7 +103,7 @@ public final class TrackingId {
}
public TrackingId.Id getTrackingId() {
- return trackingId;
+ return trackingKey;
}
public TrackingId.System getTrackingSystem() {
@@ -112,7 +112,7 @@ public final class TrackingId {
@Override
public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {trackingId, trackingSystem};
+ return new com.google.gwtorm.client.Key<?>[] {trackingKey, trackingSystem};
}
}
@@ -140,7 +140,7 @@ public final class TrackingId {
}
public String getTrackingId() {
- return key.trackingId.get();
+ return key.trackingKey.get();
}
public String getSystem() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index 2b23b7aa9e..29a426be31 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
deleted file mode 100644
index 86ea372003..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
+++ /dev/null
@@ -1,34 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountAgreementAccess extends
- Access<AccountAgreement, AccountAgreement.Key> {
- @PrimaryKey("key")
- AccountAgreement get(AccountAgreement.Key key) throws OrmException;
-
- @Query("WHERE key.accountId = ? ORDER BY acceptedOn")
- ResultSet<AccountAgreement> byAccount(Account.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
index 857475893b..fffcf9e89f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
@@ -16,7 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index b263552281..bf6c0ef8e4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -16,8 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountExternalId.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
index 9e88244f9c..1de80f3cab 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
@@ -29,10 +29,6 @@ public interface AccountGroupAccess extends
@Query("WHERE groupUUID = ?")
ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
- @Query("WHERE externalName = ?")
- ResultSet<AccountGroup> byExternalName(AccountGroup.ExternalNameKey name)
- throws OrmException;
-
@Query
ResultSet<AccountGroup> all() throws OrmException;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
deleted file mode 100644
index ecab70d6f0..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupAgreementAccess extends
- Access<AccountGroupAgreement, AccountGroupAgreement.Key> {
- @PrimaryKey("key")
- AccountGroupAgreement get(AccountGroupAgreement.Key key) throws OrmException;
-
- @Query("WHERE key.groupId = ? ORDER BY acceptedOn")
- ResultSet<AccountGroupAgreement> byGroup(AccountGroup.Id id)
- throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
index c8c7b4012d..3ee4ba0000 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
@@ -16,8 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
index 3cf24f3a41..b3f4f88dcf 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
@@ -16,8 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
index 53ecaae91b..e070d69639 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
@@ -17,8 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMember.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index f26996d359..48c4e2d910 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -17,8 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index d0bc419f14..30e685c85c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -16,7 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.client.AccountGroup.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
index ef8133b95b..80b2dc4ce1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
@@ -17,8 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountPatchReview;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountPatchReview.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
index 046d5a5b63..c07346833e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
@@ -17,9 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index d756a44571..b31b5b6c36 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -16,7 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
index 31e6c62496..db9886eff4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
index acdda5e892..0bc9981802 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
@@ -16,7 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 4660291da5..66df78a5c8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -18,9 +18,6 @@ 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.Project;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Change.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
index 6db267549c..0126a31063 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
@@ -17,8 +17,6 @@ package com.google.gerrit.reviewdb.server;
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.Change.Id;
-import com.google.gerrit.reviewdb.client.ChangeMessage.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
deleted file mode 100644
index 3c7f47a47e..0000000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
+++ /dev/null
@@ -1,33 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement.Id;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-/** Access interface for {@link ContributorAgreement}. */
-public interface ContributorAgreementAccess extends
- Access<ContributorAgreement, ContributorAgreement.Id> {
- @PrimaryKey("id")
- ContributorAgreement get(ContributorAgreement.Id key) throws OrmException;
-
- @Query("WHERE active = true ORDER BY shortName")
- ResultSet<ContributorAgreement> active() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
index 3be856332e..a5842deccc 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -18,8 +18,6 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index c04fa06395..7e0b90c595 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -17,7 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index e163bc43f1..f2b1cb7c54 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -17,7 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index b30c5bfaa2..dae8e6d51b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -18,8 +18,6 @@ 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.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 616a65614c..149626b214 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -49,9 +49,6 @@ public interface ReviewDb extends Schema {
@Relation(id = 4)
ApprovalCategoryValueAccess approvalCategoryValues();
- @Relation(id = 5)
- ContributorAgreementAccess contributorAgreements();
-
@Relation(id = 6)
AccountAccess accounts();
@@ -61,9 +58,6 @@ public interface ReviewDb extends Schema {
@Relation(id = 8)
AccountSshKeyAccess accountSshKeys();
- @Relation(id = 9)
- AccountAgreementAccess accountAgreements();
-
@Relation(id = 10)
AccountGroupAccess accountGroups();
@@ -82,9 +76,6 @@ public interface ReviewDb extends Schema {
@Relation(id = 15)
AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
- @Relation(id = 16)
- AccountGroupAgreementAccess accountGroupAgreements();
-
@Relation(id = 17)
AccountDiffPreferenceAccess accountDiffPreferences();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
index 07f255cda2..470f8c6819 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
index 88e703afbf..4010dae367 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
@@ -17,8 +17,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.StarredChange.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index e1aa9077d2..0090df8a6e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -16,8 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
index 3bb07cb0a8..4b2ed74157 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.client.SystemConfig.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
index 51babb3e02..d8b2ceea49 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
@@ -16,8 +16,6 @@ package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.TrackingId;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.TrackingId.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
@@ -31,7 +29,7 @@ public interface TrackingIdAccess extends Access<TrackingId, TrackingId.Key> {
@Query("WHERE key.changeId = ?")
ResultSet<TrackingId> byChange(Change.Id change) throws OrmException;
- @Query("WHERE key.trackingId = ?")
+ @Query("WHERE key.trackingKey = ?")
ResultSet<TrackingId> byTrackingId(TrackingId.Id trackingId)
throws OrmException;
}
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 5d0e15cbe7..9d453fc788 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -16,11 +16,6 @@ ON accounts (full_name);
-- *********************************************************************
--- AccountAgreementAccess
--- @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
-- AccountExternalIdAccess
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
@@ -32,12 +27,6 @@ ON account_external_ids (email_address);
-- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
-- AccountGroupMemberAccess
-- @PrimaryKey covers: byAccount
CREATE INDEX account_group_members_byGroup
@@ -133,13 +122,9 @@ ON patch_set_approvals (change_open, account_id, change_sort_key);
-- ChangeMessageAccess
-- @PrimaryKey covers: byChange
-
--- *********************************************************************
--- ContributorAgreementAccess
--- covers: active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
+-- covers: byPatchSet
+CREATE INDEX change_messages_byPatchset
+ON change_messages (patchset_change_id, patchset_patch_set_id);
-- *********************************************************************
-- PatchLineCommentAccess
@@ -164,8 +149,8 @@ ON patch_set_ancestors (ancestor_revision);
-- *********************************************************************
-- TrackingIdAccess
--
-CREATE INDEX tracking_ids_byTrkId
-ON tracking_ids (tracking_id);
+CREATE INDEX tracking_ids_byTrkKey
+ON tracking_ids (tracking_key);
-- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 97ad126634..2c91db4274 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -88,11 +88,6 @@ ON accounts (full_name);
-- *********************************************************************
--- AccountAgreementAccess
--- @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
-- AccountExternalIdAccess
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
@@ -104,12 +99,6 @@ ON account_external_ids (email_address);
-- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
-- AccountGroupMemberAccess
-- @PrimaryKey covers: byAccount
CREATE INDEX account_group_members_byGroup
@@ -214,13 +203,9 @@ WHERE change_open = 'N';
-- ChangeMessageAccess
-- @PrimaryKey covers: byChange
-
--- *********************************************************************
--- ContributorAgreementAccess
--- covers: active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
+-- covers: byPatchSet
+CREATE INDEX change_messages_byPatchset
+ON change_messages (patchset_change_id, patchset_patch_set_id);
-- *********************************************************************
-- PatchLineCommentAccess
@@ -247,8 +232,8 @@ ON patch_set_ancestors (ancestor_revision);
-- *********************************************************************
-- TrackingIdAccess
--
-CREATE INDEX tracking_ids_byTrkId
-ON tracking_ids (tracking_id);
+CREATE INDEX tracking_ids_byTrkKey
+ON tracking_ids (tracking_key);
-- *********************************************************************
diff --git a/gerrit-server/.gitignore b/gerrit-server/.gitignore
index 194bedcbc4..9324efed13 100644
--- a/gerrit-server/.gitignore
+++ b/gerrit-server/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-server.iml \ No newline at end of file
diff --git a/gerrit-server/.settings/org.eclipse.core.resources.prefs b/gerrit-server/.settings/org.eclipse.core.resources.prefs
index 7d5f965bcb..29abf99956 100644
--- a/gerrit-server/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-server/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 58e43cff3b..af18173a6c 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-server</artifactId>
@@ -110,6 +110,11 @@ limitations under the License.
</dependency>
<dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-antlr</artifactId>
<version>${project.version}</version>
@@ -123,6 +128,12 @@ limitations under the License.
<dependency>
<groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-extension-api</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
<artifactId>gerrit-util-cli</artifactId>
<version>${project.version}</version>
</dependency>
@@ -164,6 +175,11 @@ limitations under the License.
<groupId>com.googlecode.prolog-cafe</groupId>
<artifactId>PrologCafe</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
new file mode 100644
index 0000000000..364df807b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -0,0 +1,172 @@
+// 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.audit;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.server.CurrentUser;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AuditEvent {
+
+ public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000";
+ private static final Object UNKNOWN_RESULT = "N/A";
+
+ public final String sessionId;
+ public final CurrentUser who;
+ public final long when;
+ public final String what;
+ public final List<?> params;
+ public final Object result;
+ public final long timeAtStart;
+ public final long elapsed;
+ public final UUID uuid;
+
+ public static class UUID {
+
+ protected final String uuid;
+
+ protected UUID() {
+ uuid = String.format("audit:%s", java.util.UUID.randomUUID().toString());
+ }
+
+ public UUID(final String n) {
+ uuid = n;
+ }
+
+ public String get() {
+ return uuid;
+ }
+
+ @Override
+ public int hashCode() {
+ return uuid.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (!(obj instanceof UUID)) {
+ return false;
+ }
+
+ return uuid.equals(((UUID) obj).uuid);
+ }
+ }
+
+ /**
+ * Creates a new audit event.
+ *
+ * @param sessionId session id the event belongs to
+ * @param who principal that has generated the event
+ * @param what object of the event
+ * @param params parameters of the event
+ */
+ public AuditEvent(String sessionId, CurrentUser who, String what, List<?> params) {
+ this(sessionId, who, what, System.currentTimeMillis(), params,
+ UNKNOWN_RESULT);
+ }
+
+ /**
+ * Creates a new audit event with results
+ *
+ * @param sessionId session id the event belongs to
+ * @param who principal that has generated the event
+ * @param what object of the event
+ * @param when time-stamp of when the event started
+ * @param params parameters of the event
+ * @param result result of the event
+ */
+ public AuditEvent(String sessionId, CurrentUser who, String what, long when,
+ List<?> params, Object result) {
+ Preconditions.checkNotNull(what, "what is a mandatory not null param !");
+
+ this.sessionId = getValueWithDefault(sessionId, UNKNOWN_SESSION_ID);
+ this.who = who;
+ this.what = what;
+ this.when = when;
+ this.timeAtStart = this.when;
+ this.params = getValueWithDefault(params, Collections.emptyList());
+ this.uuid = new UUID();
+ this.result = result;
+ this.elapsed = System.currentTimeMillis() - timeAtStart;
+ }
+
+ private <T> T getValueWithDefault(T value, T defaultValueIfNull) {
+ if (value == null) {
+ return defaultValueIfNull;
+ } else {
+ return value;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return uuid.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+
+ AuditEvent other = (AuditEvent) obj;
+ return this.uuid.equals(other.uuid);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(uuid.toString());
+ sb.append("|");
+ sb.append(sessionId);
+ sb.append('|');
+ sb.append(who);
+ sb.append('|');
+ sb.append(when);
+ sb.append('|');
+ sb.append(what);
+ sb.append('|');
+ sb.append(elapsed);
+ sb.append('|');
+ if (params != null) {
+ sb.append('[');
+ for (int i = 0; i < params.size(); i++) {
+ if (i > 0) sb.append(',');
+
+ Object param = params.get(i);
+ if (param == null) {
+ sb.append("null");
+ } else {
+ sb.append(param);
+ }
+ }
+ sb.append(']');
+ }
+ sb.append('|');
+ if (result != null) {
+ sb.append(result);
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java
new file mode 100644
index 0000000000..0aab248407
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java
@@ -0,0 +1,24 @@
+// 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.audit;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface AuditListener {
+
+ void onAuditableAction(AuditEvent action);
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
new file mode 100644
index 0000000000..dc870acfc5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
@@ -0,0 +1,28 @@
+// 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.audit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+
+public class AuditModule extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ DynamicSet.setOf(binder(), AuditListener.class);
+ bind(AuditService.class);
+ }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
new file mode 100644
index 0000000000..a992aa130a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
@@ -0,0 +1,35 @@
+// 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.audit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AuditService {
+ private final DynamicSet<AuditListener> auditListeners;
+
+ @Inject
+ public AuditService(DynamicSet<AuditListener> auditListeners) {
+ this.auditListeners = auditListeners;
+ }
+
+ public void dispatch(AuditEvent action) {
+ for (AuditListener auditListener : auditListeners) {
+ auditListener.onAuditableAction(action);
+ }
+ }
+}
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 31837b0fc8..37be293eec 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
@@ -16,12 +16,12 @@ package com.google.gerrit.common;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -35,8 +35,9 @@ import com.google.gerrit.server.events.ApprovalAttribute;
import com.google.gerrit.server.events.ChangeAbandonedEvent;
import com.google.gerrit.server.events.ChangeEvent;
import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoreEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.DraftPublishedEvent;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.events.PatchSetCreatedEvent;
import com.google.gerrit.server.events.RefUpdatedEvent;
@@ -50,7 +51,6 @@ import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
@@ -99,6 +99,9 @@ public class ChangeHookRunner implements ChangeHooks {
/** Filename of the new patchset hook. */
private final File patchsetCreatedHook;
+ /** Filename of the draft published hook. */
+ private final File draftPublishedHook;
+
/** Filename of the new comments hook. */
private final File commentAddedHook;
@@ -108,7 +111,7 @@ public class ChangeHookRunner implements ChangeHooks {
/** Filename of the change abandoned hook. */
private final File changeAbandonedHook;
- /** Filename of the change abandoned hook. */
+ /** Filename of the change restored hook. */
private final File changeRestoredHook;
/** Filename of the ref updated hook. */
@@ -133,6 +136,8 @@ public class ChangeHookRunner implements ChangeHooks {
private final EventFactory eventFactory;
+ private final SitePaths sitePaths;
+
/**
* Create a new ChangeHookRunner.
*
@@ -149,7 +154,7 @@ public class ChangeHookRunner implements ChangeHooks {
final @AnonymousCowardName String anonymousCowardName,
final SitePaths sitePath, final ProjectCache projectCache,
final AccountCache accountCache, final ApprovalTypes approvalTypes,
- final EventFactory eventFactory) {
+ final EventFactory eventFactory, final SitePaths sitePaths) {
this.anonymousCowardName = anonymousCowardName;
this.repoManager = repoManager;
this.hookQueue = queue.createQueue(1, "hook");
@@ -157,10 +162,12 @@ public class ChangeHookRunner implements ChangeHooks {
this.accountCache = accountCache;
this.approvalTypes = approvalTypes;
this.eventFactory = eventFactory;
+ this.sitePaths = sitePath;
final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
patchsetCreatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "patchsetCreatedHook", "patchset-created")).getPath());
+ draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
@@ -192,16 +199,6 @@ public class ChangeHookRunner implements ChangeHooks {
}
/**
- * Get the Repository for the given change, or null on error.
- *
- * @param change Change to get repo for,
- * @return Repository or null.
- */
- private Repository openRepository(final Change change) {
- return openRepository(change.getProject());
- }
-
- /**
* Get the Repository for the given project name, or null on error.
*
* @param name Project to get repo for,
@@ -210,7 +207,7 @@ public class ChangeHookRunner implements ChangeHooks {
private Repository openRepository(final Project.NameKey name) {
try {
return repoManager.openRepository(name);
- } catch (RepositoryNotFoundException err) {
+ } catch (IOException err) {
log.warn("Cannot open repository " + name.get(), err);
return null;
}
@@ -235,9 +232,11 @@ public class ChangeHookRunner implements ChangeHooks {
final List<String> args = new ArrayList<String>();
addArg(args, "--change", event.change.id);
+ addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
addArg(args, "--change-url", event.change.url);
addArg(args, "--project", event.change.project);
addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
addArg(args, "--commit", event.patchSet.revision);
addArg(args, "--patchset", event.patchSet.number);
@@ -245,6 +244,29 @@ public class ChangeHookRunner implements ChangeHooks {
runHook(change.getProject(), patchsetCreatedHook, args);
}
+ public void doDraftPublishedHook(final Change change, final PatchSet patchSet,
+ final ReviewDb db) throws OrmException {
+ final DraftPublishedEvent event = new DraftPublishedEvent();
+ final AccountState uploader = accountCache.get(patchSet.getUploader());
+
+ event.change = eventFactory.asChangeAttribute(change);
+ event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+ event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
+ fireEvent(change, event, db);
+
+ final List<String> args = new ArrayList<String>();
+ addArg(args, "--change", event.change.id);
+ addArg(args, "--change-url", event.change.url);
+ addArg(args, "--project", event.change.project);
+ addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
+ addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
+ addArg(args, "--commit", event.patchSet.revision);
+ addArg(args, "--patchset", event.patchSet.number);
+
+ runHook(change.getProject(), draftPublishedHook, args);
+ }
+
public void doCommentAddedHook(final Change change, final Account account,
final PatchSet patchSet, final String comment, final Map<ApprovalCategory.Id,
ApprovalCategoryValue.Id> approvals, final ReviewDb db) throws OrmException {
@@ -270,6 +292,7 @@ public class ChangeHookRunner implements ChangeHooks {
addArg(args, "--change-url", event.change.url);
addArg(args, "--project", event.change.project);
addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
addArg(args, "--author", getDisplayName(account));
addArg(args, "--commit", event.patchSet.revision);
addArg(args, "--comment", comment == null ? "" : comment);
@@ -294,6 +317,7 @@ public class ChangeHookRunner implements ChangeHooks {
addArg(args, "--change-url", event.change.url);
addArg(args, "--project", event.change.project);
addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--commit", event.patchSet.revision);
@@ -314,15 +338,16 @@ public class ChangeHookRunner implements ChangeHooks {
addArg(args, "--change-url", event.change.url);
addArg(args, "--project", event.change.project);
addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
addArg(args, "--abandoner", getDisplayName(account));
addArg(args, "--reason", reason == null ? "" : reason);
runHook(change.getProject(), changeAbandonedHook, args);
}
- public void doChangeRestoreHook(final Change change, final Account account,
+ public void doChangeRestoredHook(final Change change, final Account account,
final String reason, final ReviewDb db) throws OrmException {
- final ChangeRestoreEvent event = new ChangeRestoreEvent();
+ final ChangeRestoredEvent event = new ChangeRestoredEvent();
event.change = eventFactory.asChangeAttribute(change);
event.restorer = eventFactory.asAccountAttribute(account);
@@ -334,6 +359,7 @@ public class ChangeHookRunner implements ChangeHooks {
addArg(args, "--change-url", event.change.url);
addArg(args, "--project", event.change.project);
addArg(args, "--branch", event.change.branch);
+ addArg(args, "--topic", event.change.topic);
addArg(args, "--restorer", getDisplayName(account));
addArg(args, "--reason", reason == null ? "" : reason);
@@ -370,7 +396,7 @@ public class ChangeHookRunner implements ChangeHooks {
final List<String> args = new ArrayList<String>();
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--user-id", account.getId().toString());
- addArg(args, "--cla-id", cla.getId().toString());
+ addArg(args, "--cla-name", cla.getName());
runHook(claSignedHook, args);
}
@@ -491,10 +517,12 @@ public class ChangeHookRunner implements ChangeHooks {
repo = openRepository(project);
}
+ final Map<String, String> env = pb.environment();
+ env.put("GERRIT_SITE", sitePaths.site_path.getAbsolutePath());
+
if (repo != null) {
pb.directory(repo.getDirectory());
- final Map<String, String> env = pb.environment();
env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index c424a26a53..134057dfce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -14,12 +14,12 @@
package com.google.gerrit.common;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
@@ -47,11 +47,21 @@ public interface ChangeHooks {
ReviewDb db) throws OrmException;
/**
+ * Fire the Draft Published Hook.
+ *
+ * @param change The change itself.
+ * @param patchSet The Patchset that was created.
+ * @throws OrmException
+ */
+ public void doDraftPublishedHook(Change change, PatchSet patchSet,
+ ReviewDb db) throws OrmException;
+
+ /**
* Fire the Comment Added Hook.
*
* @param change The change itself.
* @param patchSet The patchset this comment is related to.
- * @param account The gerrit user who commited the change.
+ * @param account The gerrit user who added the comment.
* @param comment The comment given.
* @param approvals Map of Approval Categories and Scores
* @throws OrmException
@@ -65,7 +75,7 @@ public interface ChangeHooks {
* Fire the Change Merged Hook.
*
* @param change The change itself.
- * @param account The gerrit user who commited the change.
+ * @param account The gerrit user who submitted the change.
* @param patchSet The patchset that was merged.
* @throws OrmException
*/
@@ -91,7 +101,7 @@ public interface ChangeHooks {
* @param reason Reason for restoring the change.
* @throws OrmException
*/
- public void doChangeRestoreHook(Change change, Account account,
+ public void doChangeRestoredHook(Change change, Account account,
String reason, ReviewDb db) throws OrmException;
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 5544216f16..f30f5eac78 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -14,11 +14,11 @@
package com.google.gerrit.common;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Branch.NameKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -46,7 +46,7 @@ public final class DisabledChangeHooks implements ChangeHooks {
}
@Override
- public void doChangeRestoreHook(Change change, Account account,
+ public void doChangeRestoredHook(Change change, Account account,
String reason, ReviewDb db) {
}
@@ -66,6 +66,11 @@ public final class DisabledChangeHooks implements ChangeHooks {
}
@Override
+ public void doDraftPublishedHook(Change change, PatchSet patchSet,
+ ReviewDb db) {
+ }
+
+ @Override
public void doRefUpdatedHook(NameKey refName, RefUpdate refUpdate,
Account account) {
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
index b720a25477..1a3ad9b929 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -14,75 +14,90 @@
package com.google.gerrit.lifecycle;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.inject.Binding;
import com.google.inject.Injector;
+import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
/** Tracks and executes registered {@link LifecycleListener}s. */
public class LifecycleManager {
- private final LinkedHashMap<LifecycleListener, Boolean> listeners =
- new LinkedHashMap<LifecycleListener, Boolean>();
+ private final List<Provider<LifecycleListener>> listeners = newList();
+ private final List<RegistrationHandle> handles = newList();
- private boolean started;
+ /** Index of the last listener to start successfully; -1 when not started. */
+ private int startedIndex = -1;
+
+ /** Add a handle that must be cleared during stop. */
+ public void add(RegistrationHandle handle) {
+ handles.add(handle);
+ }
+
+ /** Add a single listener. */
+ public void add(LifecycleListener listener) {
+ listeners.add(Providers.of(listener));
+ }
/** Add a single listener. */
- public void add(final LifecycleListener listener) {
- listeners.put(listener, true);
+ public void add(Provider<LifecycleListener> listener) {
+ listeners.add(listener);
}
/** Add all {@link LifecycleListener}s registered in the Injector. */
- public void add(final Injector injector) {
- if (started) {
- throw new IllegalStateException("Already started");
- }
- for (final Binding<LifecycleListener> binding : get(injector)) {
- add(binding.getProvider().get());
+ public void add(Injector injector) {
+ Preconditions.checkState(startedIndex < 0, "Already started");
+ for (Binding<LifecycleListener> binding : get(injector)) {
+ add(binding.getProvider());
}
}
/** Add all {@link LifecycleListener}s registered in the Injectors. */
- public void add(final Injector... injectors) {
- for (final Injector i : injectors) {
+ public void add(Injector... injectors) {
+ for (Injector i : injectors) {
add(i);
}
}
/** Start all listeners, in the order they were registered. */
public void start() {
- if (!started) {
- started = true;
- for (LifecycleListener obj : listeners.keySet()) {
- obj.start();
- }
+ for (int i = startedIndex + 1; i < listeners.size(); i++) {
+ LifecycleListener listener = listeners.get(i).get();
+ startedIndex = i;
+ listener.start();
}
}
/** Stop all listeners, in the reverse order they were registered. */
public void stop() {
- if (started) {
- final List<LifecycleListener> t =
- new ArrayList<LifecycleListener>(listeners.keySet());
-
- for (int i = t.size() - 1; 0 <= i; i--) {
- final LifecycleListener obj = t.get(i);
- try {
- obj.stop();
- } catch (Throwable err) {
- LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
- }
- }
+ for (int i = handles.size() - 1; 0 <= i; i--) {
+ handles.get(i).remove();
+ }
+ handles.clear();
- started = false;
+ for (int i = startedIndex; 0 <= i; i--) {
+ LifecycleListener obj = listeners.get(i).get();
+ try {
+ obj.stop();
+ } catch (Throwable err) {
+ LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
+ }
+ startedIndex = i - 1;
}
}
private static List<Binding<LifecycleListener>> get(Injector i) {
return i.findBindingsByType(new TypeLiteral<LifecycleListener>() {});
}
+
+ private static <T> List<T> newList() {
+ return Lists.newArrayListWithCapacity(4);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
index dcc8a356f2..04682f5a92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,5 +1,6 @@
package com.google.gerrit.lifecycle;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
import com.google.inject.binder.LinkedBindingBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index fd72e0c4b9..fbee14552f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -189,6 +189,8 @@ public class RulesCache {
git = gitMgr.openRepository(project);
} catch (RepositoryNotFoundException e) {
throw new CompileException("Cannot open repository " + project, e);
+ } catch (IOException e) {
+ throw new CompileException("Cannot open repository " + project, e);
}
try {
ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 8ab9471841..1185fd3136 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -16,19 +16,25 @@ package com.google.gerrit.rules;
import static com.google.gerrit.rules.StoredValue.create;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.ChangeData;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.SystemException;
@@ -37,21 +43,26 @@ import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import java.io.IOException;
+import java.util.Map;
+
public final class StoredValues {
public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
public static final StoredValue<Change> CHANGE = create(Change.class);
- public static final StoredValue<PatchSet.Id> PATCH_SET_ID = create(PatchSet.Id.class);
+ public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
+ public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
@Override
public PatchSetInfo createValue(Prolog engine) {
- PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+ Change change = StoredValues.CHANGE.get(engine);
+ PatchSet ps = StoredValues.PATCH_SET.get(engine);
PrologEnvironment env = (PrologEnvironment) engine.control;
PatchSetInfoFactory patchInfoFactory =
env.getInjector().getInstance(PatchSetInfoFactory.class);
try {
- return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
+ return patchInfoFactory.get(change, ps);
} catch (PatchSetInfoNotAvailableException e) {
throw new SystemException(e.getMessage());
}
@@ -70,8 +81,10 @@ public final class StoredValues {
ObjectId b = ObjectId.fromString(psInfo.getRevId());
Whitespace ws = Whitespace.IGNORE_NONE;
PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
- PatchList patchList = plCache.get(plKey);
- if (patchList == null) {
+ PatchList patchList;
+ try {
+ patchList = plCache.get(plKey);
+ } catch (PatchListNotAvailableException e) {
throw new SystemException("Cannot create " + plKey);
}
return patchList;
@@ -91,6 +104,8 @@ public final class StoredValues {
repo = gitMgr.openRepository(projectKey);
} catch (RepositoryNotFoundException e) {
throw new SystemException(e.getMessage());
+ } catch (IOException e) {
+ throw new SystemException(e.getMessage());
}
env.addToCleanup(new Runnable() {
@Override
@@ -102,6 +117,23 @@ public final class StoredValues {
}
};
+ public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
+ new StoredValue<AnonymousUser>() {
+ @Override
+ protected AnonymousUser createValue(Prolog engine) {
+ PrologEnvironment env = (PrologEnvironment) engine.control;
+ return env.getInjector().getInstance(AnonymousUser.class);
+ }
+ };
+
+ public static final StoredValue<Map<Account.Id, IdentifiedUser>> USERS =
+ new StoredValue<Map<Account.Id, IdentifiedUser>>() {
+ @Override
+ protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
+ return Maps.newHashMap();
+ }
+ };
+
private StoredValues() {
}
-} \ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
index 4d1d6319dd..ae76a6e5d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -26,8 +26,5 @@ public enum AccessPath {
SSH_COMMAND,
/** Access from a Git client using any Git protocol. */
- GIT,
-
- /** Access through replication */
- REPLICATION;
+ GIT;
}
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 3417111595..a295c49d0e 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
@@ -14,23 +14,51 @@
package com.google.gerrit.server;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
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.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
+import java.util.Set;
+/**
+ * Utility functions to manipulate patchset approvals.
+ * <p>
+ * Approvals are overloaded, they represent both approvals and reviewers
+ * which should be CCed on a change. To ensure that reviewers are not lost
+ * there must always be an approval on each patchset for each reviewer,
+ * even if the reviewer hasn't actually given a score to the change. To
+ * mark the "no score" case, a dummy approval, which may live in any of
+ * the available categories, with a score of 0 is used.
+ */
public class ApprovalsUtil {
- /* Resync the changeOpen status which is cached in the approvals table for
- performance reasons*/
- public static void syncChangeStatus(final ReviewDb db, final Change change)
+ private final ReviewDb db;
+ private final ApprovalTypes approvalTypes;
+
+ @Inject
+ ApprovalsUtil(ReviewDb db, ApprovalTypes approvalTypes) {
+ this.db = db;
+ this.approvalTypes = approvalTypes;
+ }
+
+ /**
+ * Resync the changeOpen status which is cached in the approvals table for
+ * performance reasons
+ */
+ public void syncChangeStatus(final Change change)
throws OrmException {
final List<PatchSetApproval> approvals =
db.patchSetApprovals().byChange(change.getId()).toList();
@@ -44,14 +72,13 @@ public class ApprovalsUtil {
* Moves the PatchSetApprovals to the last PatchSet on the change while
* keeping the vetos.
*
- * @param db The review database
* @param change Change to update
- * @param approvalTypes The approval types
* @throws OrmException
* @throws IOException
+ * @return List<PatchSetApproval> The previous approvals
*/
- public static void copyVetosToLatestPatchSet(final ReviewDb db, Change change,
- ApprovalTypes approvalTypes) throws OrmException, IOException {
+ public List<PatchSetApproval> copyVetosToLatestPatchSet(Change change)
+ throws OrmException, IOException {
PatchSet.Id source;
if (change.getNumberOfPatchSets() > 1) {
source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
@@ -60,15 +87,65 @@ public class ApprovalsUtil {
}
PatchSet.Id dest = change.currPatchSetId();
- for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
+ List<PatchSetApproval> patchSetApprovals = db.patchSetApprovals().byChange(change.getId()).toList();
+ for (PatchSetApproval a : patchSetApprovals) {
// ApprovalCategory.SUBMIT is still in db but not relevant in git-store
if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
final ApprovalType type = approvalTypes.byId(a.getCategoryId());
- if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+ if (a.getPatchSetId().equals(source) &&
+ type.getCategory().isCopyMinScore() &&
+ type.isMaxNegative(a)) {
db.patchSetApprovals().insert(
Collections.singleton(new PatchSetApproval(dest, a)));
}
}
}
+ return patchSetApprovals;
+ }
+
+
+ /** Attach reviewers to a change. */
+ public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+ Set<Account.Id> wantReviewers) throws OrmException {
+ Set<Id> existing = Sets.<Account.Id> newHashSet();
+ addReviewers(change, ps, info, wantReviewers, existing);
+ }
+
+ /** Attach reviewers to a change. */
+ public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+ Set<Account.Id> wantReviewers, Set<Account.Id> existingReviewers)
+ throws OrmException {
+ List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+ if (allTypes.isEmpty()) {
+ return;
+ }
+
+ Set<Account.Id> need = Sets.newHashSet(wantReviewers);
+ Account.Id authorId = info.getAuthor() != null
+ ? info.getAuthor().getAccount()
+ : null;
+ if (authorId != null && !ps.isDraft()) {
+ need.add(authorId);
+ }
+
+ Account.Id committerId = info.getCommitter() != null
+ ? info.getCommitter().getAccount()
+ : null;
+ if (committerId != null && !ps.isDraft()) {
+ need.add(committerId);
+ }
+ need.remove(change.getOwner());
+ need.removeAll(existingReviewers);
+
+ List<PatchSetApproval> cells = Lists.newArrayListWithCapacity(need.size());
+ ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId();
+ for (Account.Id account : need) {
+ PatchSetApproval psa = new PatchSetApproval(
+ new PatchSetApproval.Key(ps.getId(), account, catId),
+ (short) 0);
+ psa.cache(change);
+ cells.add(psa);
+ }
+ db.patchSetApprovals().insert(cells);
}
}
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 9dc572dfbd..535ad8e10f 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
@@ -14,9 +14,9 @@
package com.google.gerrit.server;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
@@ -30,16 +30,15 @@ import com.google.gerrit.reviewdb.client.TrackingId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.TrackingFooter;
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.git.MergeOp;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RebasedPatchSetSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.mail.ReplyToChangeSender;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
@@ -63,9 +62,13 @@ import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.ChangeIdUtil;
import org.eclipse.jgit.util.NB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -75,6 +78,9 @@ import java.util.Set;
import java.util.regex.Matcher;
public class ChangeUtil {
+
+ private static final Logger log = LoggerFactory.getLogger(ChangeUtil.class);
+
private static int uuidPrefix;
private static int uuidSeq;
@@ -191,13 +197,15 @@ public class ChangeUtil {
* Rebases a commit
*
* @param git Repository to find commits in
+ * @param inserter inserter to handle new trees and blobs.
* @param original The commit to rebase
* @param base Base to rebase against
* @return CommitBuilder the newly rebased commit
* @throws IOException Merged failed
*/
- public static CommitBuilder rebaseCommit(Repository git, RevCommit original,
- RevCommit base, PersonIdent committerIdent) throws IOException {
+ public static CommitBuilder rebaseCommit(Repository git,
+ final ObjectInserter inserter, RevCommit original, RevCommit base,
+ PersonIdent committerIdent) throws IOException {
if (original.getParentCount() == 0) {
throw new IOException(
@@ -217,6 +225,20 @@ public class ChangeUtil {
}
final ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true);
+ merger.setObjectInserter(new ObjectInserter.Filter() {
+ @Override
+ protected ObjectInserter delegate() {
+ return inserter;
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void release() {
+ }
+ });
merger.setBase(parentCommit);
merger.merge(original, base);
@@ -241,12 +263,12 @@ public class ChangeUtil {
RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
final ChangeHookRunner hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication, PersonIdent myIdent,
+ final GitReferenceUpdated replication, PersonIdent myIdent,
final ChangeControl.Factory changeControlFactory,
- final ApprovalTypes approvalTypes) throws NoSuchChangeException,
+ final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
EmailException, OrmException, MissingObjectException,
IncorrectObjectTypeException, IOException,
- PatchSetInfoNotAvailableException, InvalidChangeOperationException {
+ InvalidChangeOperationException {
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl changeControl =
@@ -314,104 +336,119 @@ public class ChangeUtil {
branchTipCommit = revWalk.parseCommit(destRef.getObjectId());
}
- final RevCommit originalCommit =
- revWalk.parseCommit(ObjectId.fromString(originalPatchSet
- .getRevision().get()));
-
- CommitBuilder rebasedCommitBuilder =
- rebaseCommit(git, originalCommit, branchTipCommit, myIdent);
-
+ final RevCommit rebasedCommit;
final ObjectInserter oi = git.newObjectInserter();
- final ObjectId rebasedCommitId;
try {
- rebasedCommitId = oi.insert(rebasedCommitBuilder);
+ ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
+ ObjectId newId = oi.insert(rebaseCommit(
+ git, oi, revWalk.parseCommit(oldId), branchTipCommit, myIdent));
oi.flush();
+ rebasedCommit = revWalk.parseCommit(newId);
} finally {
oi.release();
}
- Change updatedChange =
- db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isOpen()) {
- change.nextPatchSetId();
- return change;
- } else {
- return null;
- }
- }
- });
-
- if (updatedChange == null) {
- throw new InvalidChangeOperationException("Change is closed: "
- + change.toString());
- } else {
- change = updatedChange;
- }
+ change.nextPatchSetId();
+ final PatchSet newPatchSet = new PatchSet(change.currPatchSetId());
+ newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+ newPatchSet.setUploader(user.getAccountId());
+ newPatchSet.setRevision(new RevId(rebasedCommit.name()));
+ newPatchSet.setDraft(originalPatchSet.isDraft());
- final PatchSet rebasedPatchSet = new PatchSet(change.currPatchSetId());
- rebasedPatchSet.setCreatedOn(change.getCreatedOn());
- rebasedPatchSet.setUploader(user.getAccountId());
- rebasedPatchSet.setRevision(new RevId(rebasedCommitId.getName()));
-
- insertAncestors(db, rebasedPatchSet.getId(),
- revWalk.parseCommit(rebasedCommitId));
-
- db.patchSets().insert(Collections.singleton(rebasedPatchSet));
final PatchSetInfo info =
- patchSetInfoFactory.get(db, rebasedPatchSet.getId());
-
- change =
- db.changes().atomicUpdate(change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- change.setCurrentPatchSet(info);
- ChangeUtil.updated(change);
- return change;
- }
- });
+ patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
- final RefUpdate ru = git.updateRef(rebasedPatchSet.getRefName());
- ru.setNewObjectId(rebasedCommitId);
+ RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(rebasedCommit);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
- throw new IOException("Failed to create ref "
- + rebasedPatchSet.getRefName() + " in " + git.getDirectory()
- + ": " + ru.getResult());
+ throw new IOException(String.format(
+ "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+ change.getDest().getParentKey().get(), ru.getResult()));
}
+ replication.fire(change.getProject(), ru.getName());
- replication.scheduleUpdate(change.getProject(), ru.getName());
-
- ApprovalsUtil.copyVetosToLatestPatchSet(db, change, approvalTypes);
-
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(changeId,
- ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
- cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
- db.changeMessages().insert(Collections.singleton(cmsg));
-
- final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
- final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+ final Set<Account.Id> oldReviewers = Sets.newHashSet();
+ final Set<Account.Id> oldCC = Sets.newHashSet();
+ db.changes().beginTransaction(change.getId());
+ try {
+ Change updatedChange;
+
+ updatedChange = db.changes().atomicUpdate(changeId,
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isOpen()) {
+ change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
+ return change;
+ } else {
+ return null;
+ }
+ }
+ });
+ if (updatedChange != null) {
+ change = updatedChange;
+ } else {
+ throw new InvalidChangeOperationException(
+ String.format("Change %s is closed", change.getId()));
+ }
- for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
- if (a.getValue() != 0) {
- oldReviewers.add(a.getAccountId());
+ insertAncestors(db, newPatchSet.getId(), rebasedCommit);
+ db.patchSets().insert(Collections.singleton(newPatchSet));
+ updatedChange = db.changes().atomicUpdate(changeId,
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isClosed()) {
+ return null;
+ }
+ if (!change.currentPatchSetId().equals(patchSetId)) {
+ return null;
+ }
+ if (change.getStatus() != Change.Status.DRAFT) {
+ change.setStatus(Change.Status.NEW);
+ }
+ change.setLastSha1MergeTested(null);
+ change.setCurrentPatchSet(info);
+ ChangeUtil.updated(change);
+ return change;
+ }
+ });
+ if (updatedChange != null) {
+ change = updatedChange;
} else {
- oldCC.add(a.getAccountId());
+ throw new InvalidChangeOperationException(
+ String.format("Change %s was modified", change.getId()));
}
+
+ for (PatchSetApproval a : approvalsUtil.copyVetosToLatestPatchSet(change)) {
+ if (a.getValue() != 0) {
+ oldReviewers.add(a.getAccountId());
+ } else {
+ oldCC.add(a.getAccountId());
+ }
+ }
+
+ final ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(changeId,
+ ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+ cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
+ db.changeMessages().insert(Collections.singleton(cmsg));
+ db.commit();
+ } finally {
+ db.rollback();
}
final ReplacePatchSetSender cm =
rebasedPatchSetSenderFactory.create(change);
cm.setFrom(user.getAccountId());
- cm.setPatchSet(rebasedPatchSet);
+ cm.setPatchSet(newPatchSet);
cm.addReviewers(oldReviewers);
cm.addExtraCC(oldCC);
cm.send();
- hooks.doPatchsetCreatedHook(change, rebasedPatchSet, db);
+ hooks.doPatchsetCreatedHook(change, newPatchSet, db);
} finally {
revWalk.release();
}
@@ -425,11 +462,9 @@ public class ChangeUtil {
final RevertedSender.Factory revertedSenderFactory,
final ChangeHooks hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication, PersonIdent myIdent)
+ final GitReferenceUpdated replication, PersonIdent myIdent)
throws NoSuchChangeException, EmailException, OrmException,
- MissingObjectException, IncorrectObjectTypeException, IOException,
- PatchSetInfoNotAvailableException {
-
+ MissingObjectException, IncorrectObjectTypeException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSet patch = db.patchSets().get(patchSetId);
if (patch == null) {
@@ -441,7 +476,7 @@ public class ChangeUtil {
git = gitManager.openRepository(db.changes().get(changeId).getProject());
} catch (RepositoryNotFoundException e) {
throw new NoSuchChangeException(changeId, e);
- };
+ }
final RevWalk revWalk = new RevWalk(git);
try {
@@ -454,50 +489,62 @@ public class ChangeUtil {
RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
revWalk.parseHeaders(parentToCommitToRevert);
- CommitBuilder revertCommit = new CommitBuilder();
- revertCommit.addParentId(commitToRevert);
- revertCommit.setTreeId(parentToCommitToRevert.getTree());
- revertCommit.setAuthor(authorIdent);
- revertCommit.setCommitter(myIdent);
- revertCommit.setMessage(message);
+ CommitBuilder revertCommitBuilder = new CommitBuilder();
+ revertCommitBuilder.addParentId(commitToRevert);
+ revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
+ revertCommitBuilder.setAuthor(authorIdent);
+ revertCommitBuilder.setCommitter(myIdent);
- final ObjectInserter oi = git.newObjectInserter();;
- ObjectId id;
+ final ObjectId computedChangeId =
+ ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(),
+ commitToRevert, authorIdent, myIdent, message);
+ revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, computedChangeId, true));
+
+ RevCommit revertCommit;
+ final ObjectInserter oi = git.newObjectInserter();
try {
- id = oi.insert(revertCommit);
+ ObjectId id = oi.insert(revertCommitBuilder);
oi.flush();
+ revertCommit = revWalk.parseCommit(id);
} finally {
oi.release();
}
- Change.Key changeKey = new Change.Key("I" + id.name());
- final Change change =
- new Change(changeKey, new Change.Id(db.nextChangeId()),
- user.getAccountId(), db.changes().get(changeId).getDest());
+ final Change change = new Change(
+ new Change.Key("I" + computedChangeId.name()),
+ new Change.Id(db.nextChangeId()),
+ user.getAccountId(),
+ db.changes().get(changeId).getDest());
change.nextPatchSetId();
final PatchSet ps = new PatchSet(change.currPatchSetId());
ps.setCreatedOn(change.getCreatedOn());
- ps.setUploader(user.getAccountId());
- ps.setRevision(new RevId(id.getName()));
-
- db.patchSets().insert(Collections.singleton(ps));
+ ps.setUploader(change.getOwner());
+ ps.setRevision(new RevId(revertCommit.name()));
- final PatchSetInfo info =
- patchSetInfoFactory.get(revWalk.parseCommit(id), ps.getId());
- change.setCurrentPatchSet(info);
+ change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
ChangeUtil.updated(change);
- db.changes().insert(Collections.singleton(change));
final RefUpdate ru = git.updateRef(ps.getRefName());
- ru.setNewObjectId(id);
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(revertCommit);
ru.disableRefLog();
if (ru.update(revWalk) != RefUpdate.Result.NEW) {
- throw new IOException("Failed to create ref " + ps.getRefName()
- + " in " + git.getDirectory() + ": " + ru.getResult());
+ throw new IOException(String.format(
+ "Failed to create ref %s in %s: %s", ps.getRefName(),
+ change.getDest().getParentKey().get(), ru.getResult()));
+ }
+ replication.fire(change.getProject(), ru.getName());
+
+ 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();
}
- replication.scheduleUpdate(db.changes().get(changeId).getProject(),
- ru.getName());
final ChangeMessage cmsg =
new ChangeMessage(new ChangeMessage.Key(changeId,
@@ -505,7 +552,7 @@ public class ChangeUtil {
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
msgBuf.append("\n\n");
- msgBuf.append("This patchset was reverted in change: " + changeKey.get());
+ msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
cmsg.setMessage(msgBuf.toString());
db.changeMessages().insert(Collections.singleton(cmsg));
@@ -526,7 +573,7 @@ public class ChangeUtil {
public static void deleteDraftChange(final PatchSet.Id patchSetId,
GitRepositoryManager gitManager,
- final ReplicationQueue replication, final ReviewDb db)
+ final GitReferenceUpdated replication, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
final Change change = db.changes().get(changeId);
@@ -547,7 +594,7 @@ public class ChangeUtil {
public static void deleteOnlyDraftPatchSet(final PatchSet patch,
final Change change, GitRepositoryManager gitManager,
- final ReplicationQueue replication, final ReviewDb db)
+ final GitReferenceUpdated replication, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final PatchSet.Id patchSetId = patch.getId();
if (patch == null || !patch.isDraft()) {
@@ -570,7 +617,7 @@ public class ChangeUtil {
throw new IOException("Failed to delete ref " + patch.getRefName() +
" in " + repo.getDirectory() + ": " + update.getResult());
}
- replication.scheduleUpdate(change.getProject(), update.getName());
+ replication.fire(change.getProject(), update.getName());
} finally {
repo.close();
}
@@ -586,21 +633,21 @@ public class ChangeUtil {
public static <T extends ReplyToChangeSender> void updatedChange(
final ReviewDb db, final IdentifiedUser user, final Change change,
- final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory,
- final String err) throws NoSuchChangeException,
- InvalidChangeOperationException, EmailException, OrmException {
- if (change == null) {
- throw new InvalidChangeOperationException(err);
- }
+ final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory)
+ throws OrmException {
db.changeMessages().insert(Collections.singleton(cmsg));
- ApprovalsUtil.syncChangeStatus(db, change);
+ new ApprovalsUtil(db, null).syncChangeStatus(change);
// Email the reviewers
- final ReplyToChangeSender cm = senderFactory.create(change);
- cm.setFrom(user.getAccountId());
- cm.setChangeMessage(cmsg);
- cm.send();
+ try {
+ final ReplyToChangeSender cm = senderFactory.create(change);
+ cm.setFrom(user.getAccountId());
+ cm.setChangeMessage(cmsg);
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot email update for change " + change.getChangeId(), e);
+ }
}
public static String sortKey(long lastUpdated, int id){
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
new file mode 100644
index 0000000000..e64533c0f0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -0,0 +1,62 @@
+// 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;
+
+import com.google.gerrit.reviewdb.client.Account;
+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.server.config.FactoryModule;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.args4j.AccountGroupIdHandler;
+import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
+import com.google.gerrit.server.args4j.AccountIdHandler;
+import com.google.gerrit.server.args4j.ChangeIdHandler;
+import com.google.gerrit.server.args4j.ObjectIdHandler;
+import com.google.gerrit.server.args4j.PatchSetIdHandler;
+import com.google.gerrit.server.args4j.ProjectControlHandler;
+import com.google.gerrit.server.args4j.SocketAddressHandler;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gerrit.util.cli.OptionHandlerUtil;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import org.kohsuke.args4j.spi.OptionHandler;
+
+import java.net.SocketAddress;
+
+public class CmdLineParserModule extends FactoryModule {
+ public CmdLineParserModule() {
+ }
+
+ @Override
+ protected void configure() {
+ factory(CmdLineParser.Factory.class);
+
+ registerOptionHandler(Account.Id.class, AccountIdHandler.class);
+ registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
+ registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
+ registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
+ registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
+ registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
+ registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
+ registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
+ }
+
+ private <T> void registerOptionHandler(Class<T> type,
+ Class<? extends OptionHandler<T>> impl) {
+ install(OptionHandlerUtil.moduleFor(type, impl));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 6e519c4d4b..89cbac125e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -24,8 +26,9 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
@@ -46,13 +49,10 @@ import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.URL;
-import java.util.AbstractSet;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
@@ -70,7 +70,7 @@ public class IdentifiedUser extends CurrentUser {
private final Provider<String> canonicalUrl;
private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final GroupBackend groupBackend;
@Inject
GenericFactory(
@@ -79,14 +79,14 @@ public class IdentifiedUser extends CurrentUser {
final @AnonymousCowardName String anonymousCowardName,
final @CanonicalWebUrl Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ final GroupBackend groupBackend) {
this.capabilityControlFactory = capabilityControlFactory;
this.authConfig = authConfig;
this.anonymousCowardName = anonymousCowardName;
this.canonicalUrl = canonicalUrl;
this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
}
public IdentifiedUser create(final Account.Id id) {
@@ -96,14 +96,14 @@ public class IdentifiedUser extends CurrentUser {
public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, null, db, id);
+ groupBackend, null, db, id);
}
public IdentifiedUser create(AccessPath accessPath,
Provider<SocketAddress> remotePeerProvider, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, accessPath,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, remotePeerProvider, null, id);
+ groupBackend, remotePeerProvider, null, id);
}
}
@@ -121,7 +121,7 @@ public class IdentifiedUser extends CurrentUser {
private final Provider<String> canonicalUrl;
private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final GroupBackend groupBackend;
private final Provider<SocketAddress> remotePeerProvider;
private final Provider<ReviewDb> dbProvider;
@@ -133,7 +133,7 @@ public class IdentifiedUser extends CurrentUser {
final @AnonymousCowardName String anonymousCowardName,
final @CanonicalWebUrl Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory,
+ final GroupBackend groupBackend,
final @RemotePeer Provider<SocketAddress> remotePeerProvider,
final Provider<ReviewDb> dbProvider) {
@@ -143,7 +143,7 @@ public class IdentifiedUser extends CurrentUser {
this.canonicalUrl = canonicalUrl;
this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
this.remotePeerProvider = remotePeerProvider;
this.dbProvider = dbProvider;
@@ -153,40 +153,22 @@ public class IdentifiedUser extends CurrentUser {
final Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, accessPath,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, remotePeerProvider, dbProvider, id);
+ groupBackend, remotePeerProvider, dbProvider, id);
}
}
private static final Logger log =
LoggerFactory.getLogger(IdentifiedUser.class);
- private static final Set<AccountGroup.UUID> registeredGroups =
- new AbstractSet<AccountGroup.UUID>() {
- private final List<AccountGroup.UUID> groups =
- Collections.unmodifiableList(Arrays.asList(new AccountGroup.UUID[] {
- AccountGroup.ANONYMOUS_USERS, AccountGroup.REGISTERED_USERS}));
-
- @Override
- public boolean contains(Object o) {
- return groups.contains(o);
- }
-
- @Override
- public Iterator<AccountGroup.UUID> iterator() {
- return groups.iterator();
- }
-
- @Override
- public int size() {
- return groups.size();
- }
- };
+ private static final GroupMembership registeredGroups =
+ new ListGroupMembership(ImmutableSet.of(
+ AccountGroup.ANONYMOUS_USERS,
+ AccountGroup.REGISTERED_USERS));
private final Provider<String> canonicalUrl;
- private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
private final AuthConfig authConfig;
+ private final GroupBackend groupBackend;
private final String anonymousCowardName;
@Nullable
@@ -210,14 +192,13 @@ public class IdentifiedUser extends CurrentUser {
final String anonymousCowardName,
final Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory,
+ final GroupBackend groupBackend,
@Nullable final Provider<SocketAddress> remotePeerProvider,
@Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
super(capabilityControlFactory, accessPath);
this.canonicalUrl = canonicalUrl;
- this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
this.authConfig = authConfig;
this.anonymousCowardName = anonymousCowardName;
this.remotePeerProvider = remotePeerProvider;
@@ -225,7 +206,8 @@ public class IdentifiedUser extends CurrentUser {
this.accountId = id;
}
- private AccountState state() {
+ // TODO(cranger): maybe get the state through the accountCache instead.
+ public AccountState state() {
if (state == null) {
state = accountCache.get(getAccountId());
}
@@ -268,16 +250,23 @@ public class IdentifiedUser extends CurrentUser {
return emailAddresses;
}
+ public String getName() {
+ return new AccountInfo(getAccount()).getName(anonymousCowardName);
+ }
+
+ public String getNameEmail() {
+ return new AccountInfo(getAccount()).getNameEmail(anonymousCowardName);
+ }
+
@Override
public GroupMembership getEffectiveGroups() {
if (effectiveGroups == null) {
if (authConfig.isIdentityTrustable(state().getExternalIds())) {
- effectiveGroups = realm.groups(state());
+ effectiveGroups = groupBackend.membershipsOf(this);
} else {
- effectiveGroups = groupMembershipFactory.create(registeredGroups);
+ effectiveGroups = registeredGroups;
}
}
-
return effectiveGroups;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index fc6ac9ca5e..eba59c8095 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,40 +14,37 @@
package com.google.gerrit.server;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
-public class ReplicationUser extends CurrentUser {
- /** Magic set of groups enabling read of any project and reference. */
- public static final GroupMembership EVERYTHING_VISIBLE =
- new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
-
+/**
+ * User identity for plugin code that needs an identity.
+ * <p>
+ * An InternalUser has no real identity, it acts as the server and can access
+ * anything it wants, anytime it wants, given the JVM's own direct access to
+ * data. Plugins may use this when they need to have a CurrentUser with read
+ * permission on anything.
+ */
+public class InternalUser extends CurrentUser {
public interface Factory {
- ReplicationUser create(@Assisted GroupMembership authGroups);
+ InternalUser create();
}
- private final GroupMembership effectiveGroups;
-
@Inject
- protected ReplicationUser(CapabilityControl.Factory capabilityControlFactory,
- @Assisted GroupMembership authGroups) {
- super(capabilityControlFactory, AccessPath.REPLICATION);
- effectiveGroups = authGroups;
+ protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+ super(capabilityControlFactory, AccessPath.UNKNOWN);
}
@Override
public GroupMembership getEffectiveGroups() {
- return effectiveGroups;
+ return GroupMembership.EMPTY;
}
@Override
@@ -60,7 +57,8 @@ public class ReplicationUser extends CurrentUser {
return Collections.emptySet();
}
- public boolean isEverythingVisible() {
- return getEffectiveGroups() == EVERYTHING_VISIBLE;
+ @Override
+ public String toString() {
+ return "InternalUser";
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
new file mode 100644
index 0000000000..7e1ec4b2d0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
@@ -0,0 +1,71 @@
+// 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;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gwtjsonrpc.server.SqlTimestampDeserializer;
+
+import java.sql.Timestamp;
+
+/** Standard output format used by an API call. */
+public enum OutputFormat {
+ /**
+ * The output is a human readable text format. It may also be regular enough
+ * to be machine readable. Whether or not the text format is machine readable
+ * and will be committed to as a long term format that tools can build upon is
+ * specific to each API call.
+ */
+ TEXT,
+
+ /**
+ * Pretty-printed JSON format. This format uses whitespace to make the output
+ * readable by a human, but is also machine readable with a JSON library. The
+ * structure of the output is a long term format that tools can rely upon.
+ */
+ JSON,
+
+ /**
+ * Same as {@link #JSON}, but with unnecessary whitespace removed to save
+ * generation time and copy costs. Typically JSON_COMPACT format is used by a
+ * browser based HTML client running over the network.
+ */
+ JSON_COMPACT;
+
+ /** @return true when the format is either JSON or JSON_COMPACT. */
+ public boolean isJson() {
+ return this == JSON_COMPACT || this == JSON;
+ }
+
+ /** @return a new Gson instance configured according to the format. */
+ public GsonBuilder newGsonBuilder() {
+ if (!isJson()) {
+ throw new IllegalStateException(String.format("%s is not JSON", this));
+ }
+ GsonBuilder gb = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+ if (this == OutputFormat.JSON) {
+ gb.setPrettyPrinting();
+ }
+ return gb;
+ }
+
+ /** @return a new Gson instance configured according to the format. */
+ public Gson newGson() {
+ return newGsonBuilder().create();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 352f5404ce..75ba0d7dde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.CapabilityControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
new file mode 100644
index 0000000000..ea1f1d15db
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.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;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class ProjectUtil {
+
+ /**
+ * Checks whether the specified branch exists.
+ *
+ * @param repoManager Git repository manager to open the git repository
+ * @param branch the branch for which it should be checked if it exists
+ * @return <code>true</code> if the specified branch exists or if
+ * <code>HEAD</code> points to this branch, otherwise
+ * <code>false</code>
+ * @throws RepositoryNotFoundException the repository of the branch's project
+ * does not exist.
+ * @throws IOException error while retrieving the branch from the repository.
+ */
+ public static boolean branchExists(final GitRepositoryManager repoManager,
+ final Branch.NameKey branch) throws RepositoryNotFoundException,
+ IOException {
+ final Repository repo = repoManager.openRepository(branch.getParentKey());
+ try {
+ boolean exists = repo.getRef(branch.get()) != null;
+ if (!exists) {
+ exists = repo.getFullBranch().equals(branch.get());
+ }
+ return exists;
+ } finally {
+ repo.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
new file mode 100644
index 0000000000..fe1072d36d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -0,0 +1,54 @@
+// 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;
+
+public class StringUtil {
+ /**
+ * An array of the string representations that should be used in place
+ * of the non-printable characters in the beginning of the ASCII table
+ * when escaping a string. The index of each element in the array
+ * corresponds to its ASCII value, i.e. the string representation of
+ * ASCII 0 is found in the first element of this array.
+ */
+ static String[] NON_PRINTABLE_CHARS =
+ { "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
+ "\\b", "\\t", "\\n", "\\v", "\\f", "\\r", "\\x0e", "\\x0f",
+ "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
+ "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
+
+ /**
+ * Escapes the input string so that all non-printable characters
+ * (0x00-0x1f) are represented as a hex escape (\x00, \x01, ...)
+ * or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r).
+ * Backslashes in the input string are doubled (\\).
+ */
+ public static String escapeString(final String str) {
+ // Allocate a buffer big enough to cover the case with a string needed
+ // very excessive escaping without having to reallocate the buffer.
+ final StringBuilder result = new StringBuilder(3 * str.length());
+
+ for (int i = 0; i < str.length(); i++) {
+ char c = str.charAt(i);
+ if (c < NON_PRINTABLE_CHARS.length) {
+ result.append(NON_PRINTABLE_CHARS[c]);
+ } else if (c == '\\') {
+ result.append("\\\\");
+ } else {
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 72fb2e8a84..4827ed5d52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,45 +29,58 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collections;
-import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Translates an email address to a set of matching accounts. */
@Singleton
public class AccountByEmailCacheImpl implements AccountByEmailCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(AccountByEmailCacheImpl.class);
private static final String CACHE_NAME = "accounts_byemail";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Set<Account.Id>>> type =
- new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
- core(type, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME,
+ String.class,
+ new TypeLiteral<Set<Account.Id>>() {})
+ .loader(Loader.class);
bind(AccountByEmailCacheImpl.class);
bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
}
};
}
- private final Cache<String, Set<Account.Id>> cache;
+ private final LoadingCache<String, Set<Account.Id>> cache;
@Inject
AccountByEmailCacheImpl(
- @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
+ @Named(CACHE_NAME) LoadingCache<String, Set<Account.Id>> cache) {
this.cache = cache;
}
public Set<Account.Id> get(final String email) {
- return cache.get(email);
+ try {
+ return cache.get(email);
+ } catch (ExecutionException e) {
+ log.warn("Cannot resolve accounts by email", e);
+ return Collections.emptySet();
+ }
}
public void evict(final String email) {
- cache.remove(email);
+ if (email != null) {
+ cache.invalidate(email);
+ }
}
- static class Loader extends EntryCreator<String, Set<Account.Id>> {
+ static class Loader extends CacheLoader<String, Set<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -74,10 +89,10 @@ public class AccountByEmailCacheImpl implements AccountByEmailCache {
}
@Override
- public Set<Account.Id> createEntry(final String email) throws Exception {
+ public Set<Account.Id> load(String email) throws Exception {
final ReviewDb db = schema.open();
try {
- final HashSet<Account.Id> r = new HashSet<Account.Id>();
+ Set<Account.Id> r = Sets.newHashSet();
for (Account a : db.accounts().byPreferredEmail(email)) {
r.add(a.getId());
}
@@ -85,30 +100,10 @@ public class AccountByEmailCacheImpl implements AccountByEmailCache {
.byEmailAddress(email)) {
r.add(a.getAccountId());
}
- return pack(r);
+ return ImmutableSet.copyOf(r);
} finally {
db.close();
}
}
-
- @Override
- public Set<Account.Id> missing(final String key) {
- return Collections.emptySet();
- }
-
- private static Set<Account.Id> pack(final Set<Account.Id> c) {
- switch (c.size()) {
- case 0:
- return Collections.emptySet();
- case 1:
- return one(c);
- default:
- return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
- }
- }
-
- private static <T> Set<T> one(final Set<T> c) {
- return Collections.singleton(c.iterator().next());
- }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 819ec314e1..4217f9f8d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,14 +14,16 @@
package com.google.gerrit.server.account;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -30,14 +32,21 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Caches important (but small) account state to avoid database hits. */
@Singleton
public class AccountCacheImpl implements AccountCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(AccountCacheImpl.class);
+
private static final String BYID_NAME = "accounts";
private static final String BYUSER_NAME = "accounts_byname";
@@ -45,13 +54,13 @@ public class AccountCacheImpl implements AccountCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
- new TypeLiteral<Cache<Account.Id, AccountState>>() {};
- core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+ cache(BYID_NAME, Account.Id.class, AccountState.class)
+ .loader(ByIdLoader.class);
- final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
- new TypeLiteral<Cache<String, Account.Id>>() {};
- core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+ cache(BYUSER_NAME,
+ String.class,
+ new TypeLiteral<Optional<Account.Id>>() {})
+ .loader(ByNameLoader.class);
bind(AccountCacheImpl.class);
bind(AccountCache.class).to(AccountCacheImpl.class);
@@ -59,54 +68,76 @@ public class AccountCacheImpl implements AccountCache {
};
}
- private final Cache<Account.Id, AccountState> byId;
- private final Cache<String, Account.Id> byName;
+ private final LoadingCache<Account.Id, AccountState> byId;
+ private final LoadingCache<String, Optional<Account.Id>> byName;
@Inject
- AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
- @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+ AccountCacheImpl(@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
+ @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
this.byId = byId;
this.byName = byUsername;
}
- public AccountState get(final Account.Id accountId) {
- return byId.get(accountId);
+ public AccountState get(Account.Id accountId) {
+ try {
+ return byId.get(accountId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load AccountState for " + accountId, e);
+ return missing(accountId);
+ }
}
@Override
public AccountState getByUsername(String username) {
- Account.Id id = byName.get(username);
- return id != null ? byId.get(id) : null;
+ try {
+ Optional<Account.Id> id = byName.get(username);
+ return id != null && id.isPresent() ? byId.get(id.get()) : null;
+ } catch (ExecutionException e) {
+ log.warn("Cannot load AccountState for " + username, e);
+ return null;
+ }
}
- public void evict(final Account.Id accountId) {
- byId.remove(accountId);
+ public void evict(Account.Id accountId) {
+ if (accountId != null) {
+ byId.invalidate(accountId);
+ }
}
public void evictByUsername(String username) {
- byName.remove(username);
+ if (username != null) {
+ byName.invalidate(username);
+ }
}
- static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+ private static AccountState missing(Account.Id accountId) {
+ Account account = new Account(accountId);
+ Collection<AccountExternalId> ids = Collections.emptySet();
+ Set<AccountGroup.UUID> anon = ImmutableSet.of(AccountGroup.ANONYMOUS_USERS);
+ return new AccountState(account, anon, ids);
+ }
+
+ static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
private final SchemaFactory<ReviewDb> schema;
private final GroupCache groupCache;
- private final Cache<String, Account.Id> byName;
+ private final LoadingCache<String, Optional<Account.Id>> byName;
@Inject
ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
- @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+ @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
this.schema = sf;
this.groupCache = groupCache;
this.byName = byUsername;
}
@Override
- public AccountState createEntry(final Account.Id key) throws Exception {
+ public AccountState load(Account.Id key) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountState state = load(db, key);
- if (state.getUserName() != null) {
- byName.put(state.getUserName(), state.getAccount().getId());
+ String user = state.getUserName();
+ if (user != null) {
+ byName.put(user, Optional.of(state.getAccount().getId()));
}
return state;
} finally {
@@ -142,18 +173,9 @@ public class AccountCacheImpl implements AccountCache {
return new AccountState(account, internalGroups, externalIds);
}
-
- @Override
- public AccountState missing(final Account.Id accountId) {
- final Account account = new Account(accountId);
- final Collection<AccountExternalId> ids = Collections.emptySet();
- final Set<AccountGroup.UUID> anonymous =
- Collections.singleton(AccountGroup.ANONYMOUS_USERS);
- return new AccountState(account, anonymous, ids);
- }
}
- static class ByNameLoader extends EntryCreator<String, Account.Id> {
+ static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -162,14 +184,17 @@ public class AccountCacheImpl implements AccountCache {
}
@Override
- public Account.Id createEntry(final String username) throws Exception {
+ public Optional<Account.Id> load(String username) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountExternalId.Key key = new AccountExternalId.Key( //
AccountExternalId.SCHEME_USERNAME, //
username);
final AccountExternalId id = db.accountExternalIds().get(key);
- return id != null ? id.getAccountId() : null;
+ if (id != null) {
+ return Optional.of(id.getAccountId());
+ }
+ return Optional.absent();
} finally {
db.close();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index b297ed8391..32b4e2c3f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.AccountsSection;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -27,16 +30,19 @@ import java.util.Set;
/** Access control management for one account's access to other accounts. */
public class AccountControl {
public static class Factory {
+ private final ProjectCache projectCache;
private final GroupControl.Factory groupControlFactory;
private final Provider<CurrentUser> user;
private final IdentifiedUser.GenericFactory userFactory;
private final AccountVisibility accountVisibility;
@Inject
- Factory(final GroupControl.Factory groupControlFactory,
+ Factory(final ProjectCache projectCache,
+ final GroupControl.Factory groupControlFactory,
final Provider<CurrentUser> user,
final IdentifiedUser.GenericFactory userFactory,
final AccountVisibility accountVisibility) {
+ this.projectCache = projectCache;
this.groupControlFactory = groupControlFactory;
this.user = user;
this.userFactory = userFactory;
@@ -44,20 +50,24 @@ public class AccountControl {
}
public AccountControl get() {
- return new AccountControl(groupControlFactory, user.get(), userFactory,
- accountVisibility);
+ return new AccountControl(projectCache, groupControlFactory, user.get(),
+ userFactory, accountVisibility);
}
}
+ private final AccountsSection accountsSection;
private final GroupControl.Factory groupControlFactory;
private final CurrentUser currentUser;
private final IdentifiedUser.GenericFactory userFactory;
private final AccountVisibility accountVisibility;
- AccountControl(final GroupControl.Factory groupControlFactory,
+ AccountControl(final ProjectCache projectCache,
+ final GroupControl.Factory groupControlFactory,
final CurrentUser currentUser,
final IdentifiedUser.GenericFactory userFactory,
final AccountVisibility accountVisibility) {
+ this.accountsSection =
+ projectCache.getAllProjects().getConfig().getAccountsSection();
this.groupControlFactory = groupControlFactory;
this.currentUser = currentUser;
this.userFactory = userFactory;
@@ -73,10 +83,21 @@ public class AccountControl {
* effective groups.
*/
public boolean canSee(final Account otherUser) {
+ return canSee(otherUser.getId());
+ }
+
+ /**
+ * Returns true if the otherUser is allowed to see the current user, based
+ * on the account visibility policy. Depending on the group membership
+ * realms supported, this may not be able to determine SAME_GROUP or
+ * VISIBLE_GROUP correctly (defaulting to not being visible). This is because
+ * {@link GroupMembership#getKnownGroups()} may only return a subset of the
+ * effective groups.
+ */
+ public boolean canSee(final Account.Id otherUser) {
// Special case: I can always see myself.
if (currentUser instanceof IdentifiedUser
- && ((IdentifiedUser) currentUser).getAccountId()
- .equals(otherUser.getId())) {
+ && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
return true;
}
@@ -87,6 +108,12 @@ public class AccountControl {
Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser);
usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
usersGroups.remove(AccountGroup.REGISTERED_USERS);
+ for (PermissionRule rule : accountsSection.getSameGroupVisibility()) {
+ if (rule.isBlock() || rule.isDeny()) {
+ usersGroups.remove(rule.getGroup().getUUID());
+ }
+ }
+
if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
return true;
}
@@ -115,7 +142,7 @@ public class AccountControl {
return false;
}
- private Set<AccountGroup.UUID> groupsOf(Account account) {
- return userFactory.create(account.getId()).getEffectiveGroups().getKnownGroups();
+ private Set<AccountGroup.UUID> groupsOf(Account.Id account) {
+ return userFactory.create(account).getEffectiveGroups().getKnownGroups();
}
}
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 6c216a86d4..e469c34155 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
@@ -426,6 +426,56 @@ public class AccountManager {
}
}
+ /**
+ * Unlink an authentication identity from an existing account.
+ *
+ * @param from account to unlink the identity from.
+ * @param who the identity to delete
+ * @return the result of unlinking the identity from the user.
+ * @throws AccountException the identity belongs to a different account, or it
+ * cannot be unlinked at this time.
+ */
+ public AuthResult unlink(final Account.Id from, AuthRequest who)
+ throws AccountException {
+ try {
+ final ReviewDb db = schema.open();
+ try {
+ who = realm.unlink(db, from, who);
+
+ final AccountExternalId.Key key = id(who);
+ AccountExternalId extId = db.accountExternalIds().get(key);
+ if (extId != null) {
+ if (!extId.getAccountId().equals(from)) {
+ throw new AccountException("Identity in use by another account");
+ }
+ db.accountExternalIds().delete(Collections.singleton(extId));
+
+ if (who.getEmailAddress() != null) {
+ final Account a = db.accounts().get(from);
+ if (a.getPreferredEmail() != null
+ && a.getPreferredEmail().equals(who.getEmailAddress())) {
+ a.setPreferredEmail(null);
+ db.accounts().update(Collections.singleton(a));
+ }
+ byEmailCache.evict(who.getEmailAddress());
+ byIdCache.evict(from);
+ }
+
+ } else {
+ throw new AccountException("Identity not found");
+ }
+
+ return new AuthResult(from, key, false);
+
+ } finally {
+ db.close();
+ }
+ } catch (OrmException e) {
+ throw new AccountException("Cannot unlink identity", e);
+ }
+ }
+
+
private static AccountExternalId.Key id(final AuthRequest who) {
return new AccountExternalId.Key(who.getExternalId());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 3a2ac56d87..abdf29ead5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -115,7 +116,21 @@ public class AccountResolver {
final int lt = nameOrEmail.indexOf('<');
final int gt = nameOrEmail.indexOf('>');
if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
- return byEmail.get(nameOrEmail.substring(lt + 1, gt));
+ Set<Account.Id> ids = byEmail.get(nameOrEmail.substring(lt + 1, gt));
+ if (ids.isEmpty() || ids.size() == 1) {
+ return ids;
+ }
+
+ // more than one match, try to return the best one
+ String name = nameOrEmail.substring(0, lt - 1);
+ Set<Account.Id> nameMatches = Sets.newHashSet();
+ for (Account.Id id : ids) {
+ Account a = byId.get(id).getAccount();
+ if (name.equals(a.getFullName())) {
+ nameMatches.add(id);
+ }
+ }
+ return nameMatches.isEmpty() ? ids : nameMatches;
}
if (nameOrEmail.contains("@")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java
new file mode 100644
index 0000000000..fdaabd2929
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java
@@ -0,0 +1,30 @@
+// 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.account;
+
+/** Method by which a user has authenticated for a given request. */
+public enum AuthMethod {
+ /** The user is not authenticated */
+ NONE,
+
+ /** The user is authenticated via a cookie. */
+ COOKIE,
+
+ /** The user authenticated with a password for this request. */
+ PASSWORD,
+
+ /** The user has used a credentialess development feature to login. */
+ BACKDOOR;
+}
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 eb429214e2..1524185261 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
@@ -83,7 +83,7 @@ public class CapabilityControl {
|| canAdministrateServer();
}
- /** @return true if the user can create a group. */
+ /** @return true if the user can create a project. */
public boolean canCreateProject() {
return canPerform(GlobalCapability.CREATE_PROJECT)
|| canAdministrateServer();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 56a95ba4fc..c90f3e9784 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -15,26 +15,20 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.inject.Inject;
-import java.util.Collections;
-import java.util.List;
import java.util.Set;
public class DefaultRealm implements Realm {
private final EmailExpander emailExpander;
private final AccountByEmailCache byEmail;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
@Inject
DefaultRealm(final EmailExpander emailExpander,
- final AccountByEmailCache byEmail,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ final AccountByEmailCache byEmail) {
this.emailExpander = emailExpander;
this.byEmail = byEmail;
- this.groupMembershipFactory = groupMembershipFactory;
}
@Override
@@ -57,12 +51,12 @@ public class DefaultRealm implements Realm {
}
@Override
- public void onCreateAccount(final AuthRequest who, final Account account) {
+ public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+ return who;
}
@Override
- public GroupMembership groups(final AccountState who) {
- return groupMembershipFactory.create(who.getInternalGroups());
+ public void onCreateAccount(final AuthRequest who, final Account account) {
}
@Override
@@ -75,9 +69,4 @@ public class DefaultRealm implements Realm {
}
return null;
}
-
- @Override
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
- return Collections.emptySet();
- }
}
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
new file mode 100644
index 0000000000..b4e770fdbe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -0,0 +1,51 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GroupDescription;
+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 java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementations of GroupBackend provide lookup and membership accessors
+ * to a group system.
+ */
+@ExtensionPoint
+public interface GroupBackend {
+ /** @return {@code true} if the backend can operate on the UUID. */
+ boolean handles(AccountGroup.UUID uuid);
+
+ /**
+ * Looks up a group in the backend. If the group does not exist, null is
+ * returned.
+ *
+ * @param uuid the group identifier
+ * @return the group
+ */
+ @Nullable
+ GroupDescription.Basic get(AccountGroup.UUID uuid);
+
+ /** @return suggestions for the group name sorted by name. */
+ Collection<GroupReference> suggest(String name);
+
+ /** @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
new file mode 100644
index 0000000000..cdbb0e49bc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -0,0 +1,89 @@
+// 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.account;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.GroupReference;
+
+import java.util.Collection;
+import java.util.Comparator;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for dealing with a GroupBackend.
+ */
+public class GroupBackends {
+
+ public static final Comparator<GroupReference> GROUP_REF_NAME_COMPARATOR =
+ new Comparator<GroupReference>() {
+ @Override
+ public int compare(GroupReference a, GroupReference b) {
+ return a.getName().compareTo(b.getName());
+ }
+ };
+
+ /**
+ * Runs {@link GroupBackend#suggest(String)} 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
+ * @return the best single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findBestSuggestion(
+ GroupBackend groupBackend, String name) {
+ Collection<GroupReference> refs = groupBackend.suggest(name);
+ if (refs.size() == 1) {
+ return Iterables.getOnlyElement(refs);
+ }
+
+ for (GroupReference ref : refs) {
+ if (isExactSuggestion(ref, name)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Runs {@link GroupBackend#suggest(String)} 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
+ * @return the exact single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findExactSuggestion(
+ GroupBackend groupBackend, String name) {
+ Collection<GroupReference> refs = groupBackend.suggest(name);
+ for (GroupReference ref : refs) {
+ if (isExactSuggestion(ref, name)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /** Returns whether the GroupReference is an exact suggestion for the name. */
+ public static boolean isExactSuggestion(GroupReference ref, String name) {
+ return ref.getName().equalsIgnoreCase(name) || ref.getUUID().get().equals(name);
+ }
+
+ private GroupBackends() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index b092ac4162..3b9e85fcde 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -16,8 +16,6 @@ package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.util.Collection;
-
import javax.annotation.Nullable;
/** Tracks group objects in memory for efficient access. */
@@ -34,8 +32,6 @@ public interface GroupCache {
@Nullable
public AccountGroup get(AccountGroup.UUID uuid);
- public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
-
/** @return sorted iteration of groups. */
public abstract Iterable<AccountGroup> all();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d29a5e59b9..b301839616 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.account;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,48 +30,41 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
-import java.util.ArrayList;
-import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collections;
import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.ExecutionException;
/** Tracks group objects in memory for efficient access. */
@Singleton
public class GroupCacheImpl implements GroupCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(GroupCacheImpl.class);
+
private static final String BYID_NAME = "groups";
private static final String BYNAME_NAME = "groups_byname";
private static final String BYUUID_NAME = "groups_byuuid";
- private static final String BYEXT_NAME = "groups_byext";
- private static final String BYNAME_LIST = "groups_byname_list";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
- new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
- core(byId, BYID_NAME).populateWith(ByIdLoader.class);
-
- final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
- new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
- core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+ cache(BYID_NAME,
+ AccountGroup.Id.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByIdLoader.class);
- final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
- new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
- core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
+ cache(BYNAME_NAME,
+ String.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByNameLoader.class);
- final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
- new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
- core(byExternalName, BYEXT_NAME) //
- .populateWith(ByExternalNameLoader.class);
-
- final TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>> listType =
- new TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>>() {};
- core(listType, BYNAME_LIST).populateWith(Lister.class);
+ cache(BYUUID_NAME,
+ String.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByUUIDLoader.class);
bind(GroupCacheImpl.class);
bind(GroupCache.class).to(GroupCacheImpl.class);
@@ -76,94 +72,113 @@ public class GroupCacheImpl implements GroupCache {
};
}
- private final Cache<AccountGroup.Id, AccountGroup> byId;
- private final Cache<AccountGroup.NameKey, AccountGroup> byName;
- private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
- private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
- private final Cache<ListKey,SortedSet<AccountGroup.NameKey>> list;
- private final Lock listLock;
+ private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
+ private final LoadingCache<String, Optional<AccountGroup>> byName;
+ private final LoadingCache<String, Optional<AccountGroup>> byUUID;
+ private final SchemaFactory<ReviewDb> schema;
@Inject
GroupCacheImpl(
- @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
- @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
- @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
- @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
- @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
+ @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
+ @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID,
+ SchemaFactory<ReviewDb> schema) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
- this.byExternalName = byExternalName;
- this.list = list;
- this.listLock = new ReentrantLock(true /* fair */);
+ this.schema = schema;
}
+ @Override
public AccountGroup get(final AccountGroup.Id groupId) {
- return byId.get(groupId);
+ try {
+ Optional<AccountGroup> g = byId.get(groupId);
+ return g.isPresent() ? g.get() : missing(groupId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load group "+groupId, e);
+ return missing(groupId);
+ }
}
+ @Override
public void evict(final AccountGroup group) {
- byId.remove(group.getId());
- byName.remove(group.getNameKey());
- byUUID.remove(group.getGroupUUID());
- byExternalName.remove(group.getExternalNameKey());
+ if (group.getId() != null) {
+ byId.invalidate(group.getId());
+ }
+ if (group.getNameKey() != null) {
+ byName.invalidate(group.getNameKey().get());
+ }
+ if (group.getGroupUUID() != null) {
+ byUUID.invalidate(group.getGroupUUID().get());
+ }
}
+ @Override
public void evictAfterRename(final AccountGroup.NameKey oldName,
final AccountGroup.NameKey newName) {
- byName.remove(oldName);
- updateGroupList(oldName, newName);
- }
-
- public AccountGroup get(final AccountGroup.NameKey name) {
- return byName.get(name);
+ if (oldName != null) {
+ byName.invalidate(oldName.get());
+ }
+ if (newName != null) {
+ byName.invalidate(newName.get());
+ }
}
- public AccountGroup get(final AccountGroup.UUID uuid) {
- return byUUID.get(uuid);
+ @Override
+ public AccountGroup get(AccountGroup.NameKey name) {
+ if (name == null) {
+ return null;
+ }
+ try {
+ return byName.get(name.get()).orNull();
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
+ return null;
+ }
}
- public Collection<AccountGroup> get(
- final AccountGroup.ExternalNameKey externalName) {
- return byExternalName.get(externalName);
+ @Override
+ public AccountGroup get(AccountGroup.UUID uuid) {
+ if (uuid == null) {
+ return null;
+ }
+ try {
+ return byUUID.get(uuid.get()).orNull();
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
+ return null;
+ }
}
@Override
public Iterable<AccountGroup> all() {
- final List<AccountGroup> groups = new ArrayList<AccountGroup>();
- for (final AccountGroup.NameKey groupName : list.get(ListKey.ALL)) {
- final AccountGroup group = get(groupName);
- if (group != null) {
- groups.add(group);
+ try {
+ ReviewDb db = schema.open();
+ try {
+ return Collections.unmodifiableList(db.accountGroups().all().toList());
+ } finally {
+ db.close();
}
+ } catch (OrmException e) {
+ log.warn("Cannot list internal groups", e);
+ return Collections.emptyList();
}
- return Collections.unmodifiableList(groups);
}
@Override
- public void onCreateGroup(final AccountGroup.NameKey newGroupName) {
- updateGroupList(null, newGroupName);
+ public void onCreateGroup(AccountGroup.NameKey newGroupName) {
+ byName.invalidate(newGroupName.get());
}
- private void updateGroupList(final AccountGroup.NameKey nameToRemove,
- final AccountGroup.NameKey nameToAdd) {
- listLock.lock();
- try {
- SortedSet<AccountGroup.NameKey> n = list.get(ListKey.ALL);
- n = new TreeSet<AccountGroup.NameKey>(n);
- if (nameToRemove != null) {
- n.remove(nameToRemove);
- }
- if (nameToAdd != null) {
- n.add(nameToAdd);
- }
- list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
- } finally {
- listLock.unlock();
- }
+ private static AccountGroup missing(AccountGroup.Id key) {
+ AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
+ AccountGroup g = new AccountGroup(name, key, null);
+ g.setType(AccountGroup.Type.SYSTEM);
+ return g;
}
- static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
+ static class ByIdLoader extends
+ CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -172,32 +187,18 @@ public class GroupCacheImpl implements GroupCache {
}
@Override
- public AccountGroup createEntry(final AccountGroup.Id key) throws Exception {
+ public Optional<AccountGroup> load(final AccountGroup.Id key)
+ throws Exception {
final ReviewDb db = schema.open();
try {
- final AccountGroup group = db.accountGroups().get(key);
- if (group != null) {
- return group;
- } else {
- return missing(key);
- }
+ return Optional.fromNullable(db.accountGroups().get(key));
} finally {
db.close();
}
}
-
- @Override
- public AccountGroup missing(final AccountGroup.Id key) {
- final AccountGroup.NameKey name =
- new AccountGroup.NameKey("Deleted Group" + key.toString());
- final AccountGroup g = new AccountGroup(name, key, null);
- g.setType(AccountGroup.Type.SYSTEM);
- return g;
- }
}
- static class ByNameLoader extends
- EntryCreator<AccountGroup.NameKey, AccountGroup> {
+ static class ByNameLoader extends CacheLoader<String, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -206,25 +207,23 @@ public class GroupCacheImpl implements GroupCache {
}
@Override
- public AccountGroup createEntry(final AccountGroup.NameKey key)
+ public Optional<AccountGroup> load(String name)
throws Exception {
- final AccountGroupName r;
final ReviewDb db = schema.open();
try {
- r = db.accountGroupNames().get(key);
+ AccountGroup.NameKey key = new AccountGroup.NameKey(name);
+ AccountGroupName r = db.accountGroupNames().get(key);
if (r != null) {
- return db.accountGroups().get(r.getId());
- } else {
- return null;
+ return Optional.fromNullable(db.accountGroups().get(r.getId()));
}
+ return Optional.absent();
} finally {
db.close();
}
}
}
- static class ByUUIDLoader extends
- EntryCreator<AccountGroup.UUID, AccountGroup> {
+ static class ByUUIDLoader extends CacheLoader<String, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -233,71 +232,20 @@ public class GroupCacheImpl implements GroupCache {
}
@Override
- public AccountGroup createEntry(final AccountGroup.UUID uuid)
+ public Optional<AccountGroup> load(String uuid)
throws Exception {
final ReviewDb db = schema.open();
try {
- List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+ List<AccountGroup> r;
+
+ r = db.accountGroups().byUUID(new AccountGroup.UUID(uuid)).toList();
if (r.size() == 1) {
- return r.get(0);
+ return Optional.of(r.get(0));
+ } else if (r.size() == 0) {
+ return Optional.absent();
} else {
- return null;
- }
- } finally {
- db.close();
- }
- }
- }
-
- static class ByExternalNameLoader extends
- EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
- private final SchemaFactory<ReviewDb> schema;
-
- @Inject
- ByExternalNameLoader(final SchemaFactory<ReviewDb> sf) {
- schema = sf;
- }
-
- @Override
- public Collection<AccountGroup> createEntry(
- final AccountGroup.ExternalNameKey key) throws Exception {
- final ReviewDb db = schema.open();
- try {
- return db.accountGroups().byExternalName(key).toList();
- } finally {
- db.close();
- }
- }
- }
-
- static class ListKey {
- static final ListKey ALL = new ListKey();
-
- private ListKey() {
- }
- }
-
- static class Lister extends EntryCreator<ListKey, SortedSet<AccountGroup.NameKey>> {
- private final SchemaFactory<ReviewDb> schema;
-
- @Inject
- Lister(final SchemaFactory<ReviewDb> sf) {
- schema = sf;
- }
-
- @Override
- public SortedSet<AccountGroup.NameKey> createEntry(ListKey key)
- throws Exception {
- final ReviewDb db = schema.open();
- try {
- final List<AccountGroupName> groupNames =
- db.accountGroupNames().all().toList();
- final SortedSet<AccountGroup.NameKey> groups =
- new TreeSet<AccountGroup.NameKey>();
- for (final AccountGroupName groupName : groupNames) {
- groups.add(groupName.getNameKey());
+ throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
}
- return Collections.unmodifiableSortedSet(groups);
} finally {
db.close();
}
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 f7451a8488..d9b12ac338 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
@@ -14,6 +14,8 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,11 +29,14 @@ public class GroupControl {
public static class Factory {
private final GroupCache groupCache;
private final Provider<CurrentUser> user;
+ private final GroupBackend groupBackend;
@Inject
- Factory(final GroupCache gc, final Provider<CurrentUser> cu) {
+ Factory(final GroupCache gc, final Provider<CurrentUser> cu,
+ final GroupBackend gb) {
groupCache = gc;
user = cu;
+ groupBackend = gb;
}
public GroupControl controlFor(final AccountGroup.Id groupId)
@@ -40,20 +45,20 @@ public class GroupControl {
if (group == null) {
throw new NoSuchGroupException(groupId);
}
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl controlFor(final AccountGroup.UUID groupId)
throws NoSuchGroupException {
- final AccountGroup group = groupCache.get(groupId);
+ final GroupDescription.Basic group = groupBackend.get(groupId);
if (group == null) {
throw new NoSuchGroupException(groupId);
}
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl controlFor(final AccountGroup group) {
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -66,23 +71,21 @@ public class GroupControl {
}
}
- private final GroupCache groupCache;
private final CurrentUser user;
- private final AccountGroup group;
+ private final GroupDescription.Basic group;
private Boolean isOwner;
- GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
- groupCache = g;
+ GroupControl(CurrentUser who, GroupDescription.Basic gd) {
user = who;
- group = gc;
+ group = gd;
}
- public CurrentUser getCurrentUser() {
- return user;
+ GroupControl(CurrentUser who, AccountGroup ag) {
+ this(who, GroupDescriptions.forAccountGroup(ag));
}
- public AccountGroup getAccountGroup() {
- return group;
+ public CurrentUser getCurrentUser() {
+ return user;
}
/** Can this user see this group exists? */
@@ -93,9 +96,11 @@ public class GroupControl {
}
public boolean isOwner() {
- if (isOwner == null) {
- AccountGroup g = groupCache.get(group.getOwnerGroupId());
- AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+ AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+ if (accountGroup == null) {
+ isOwner = false;
+ } else if (isOwner == null) {
+ AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
|| getCurrentUser().getCapabilities().canAdministrateServer();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 7f88134153..2e500bdb43 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -39,6 +41,7 @@ public class GroupDetailFactory implements Callable<GroupDetail> {
private final ReviewDb db;
private final GroupControl.Factory groupControl;
private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final AccountInfoCacheFactory aic;
private final GroupInfoCacheFactory gic;
@@ -48,12 +51,14 @@ public class GroupDetailFactory implements Callable<GroupDetail> {
@Inject
GroupDetailFactory(final ReviewDb db,
final GroupControl.Factory groupControl, final GroupCache groupCache,
+ final GroupBackend groupBackend,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final GroupInfoCacheFactory.Factory groupInfoCacheFactory,
@Assisted final AccountGroup.Id groupId) {
this.db = db;
this.groupControl = groupControl;
this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.aic = accountInfoCacheFactory.create();
this.gic = groupInfoCacheFactory.create();
@@ -63,10 +68,13 @@ public class GroupDetailFactory implements Callable<GroupDetail> {
@Override
public GroupDetail call() throws OrmException, NoSuchGroupException {
control = groupControl.validateFor(groupId);
- final AccountGroup group = control.getAccountGroup();
+ final AccountGroup group = groupCache.get(groupId);
final GroupDetail detail = new GroupDetail();
detail.setGroup(group);
- detail.setOwnerGroup(groupCache.get(group.getOwnerGroupId()));
+ GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
+ if (ownerGroup != null) {
+ detail.setOwnerGroup(GroupReference.forGroup(ownerGroup));
+ }
switch (group.getType()) {
case INTERNAL:
detail.setMembers(loadMembers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 791d0f59db..7fbba450a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,24 +29,30 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Tracks group inclusions in memory for efficient access. */
@Singleton
public class GroupIncludeCacheImpl implements GroupIncludeCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(GroupIncludeCacheImpl.class);
private static final String BYINCLUDE_NAME = "groups_byinclude";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
- new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
- core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
+ cache(BYINCLUDE_NAME,
+ AccountGroup.UUID.class,
+ new TypeLiteral<Set<AccountGroup.UUID>>() {})
+ .loader(ByIncludeLoader.class);
bind(GroupIncludeCacheImpl.class);
bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
@@ -52,24 +60,31 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
};
}
- private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
+ private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
@Inject
GroupIncludeCacheImpl(
- @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
+ @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
this.byInclude = byInclude;
}
public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
- return byInclude.get(groupId);
+ try {
+ return byInclude.get(groupId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load included groups", e);
+ return Collections.emptySet();
+ }
}
public void evictInclude(AccountGroup.UUID groupId) {
- byInclude.remove(groupId);
+ if (groupId != null) {
+ byInclude.invalidate(groupId);
+ }
}
static class ByIncludeLoader extends
- EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
+ CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -78,32 +93,28 @@ public class GroupIncludeCacheImpl implements GroupIncludeCache {
}
@Override
- public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
+ public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
final ReviewDb db = schema.open();
try {
List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
if (group.size() != 1) {
- return Collections.emptyList();
+ return Collections.emptySet();
}
- Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
- for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+ Set<AccountGroup.Id> ids = Sets.newHashSet();
+ for (AccountGroupInclude agi : db.accountGroupIncludes()
+ .byInclude(group.get(0).getId())) {
ids.add(agi.getGroupId());
}
- Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+ Set<AccountGroup.UUID> groupArray = Sets.newHashSet();
for (AccountGroup g : db.accountGroups().get(ids)) {
groupArray.add(g.getGroupUUID());
}
- return Collections.unmodifiableCollection(groupArray);
+ return ImmutableSet.copyOf(groupArray);
} finally {
db.close();
}
}
-
- @Override
- public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
- return Collections.emptyList();
- }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index 9bb571ea7d..d536c09109 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,6 @@ import java.util.Set;
* the presence of a user in a particular group.
*/
public interface GroupMembership {
-
public static final GroupMembership EMPTY =
new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
@@ -45,7 +44,7 @@ public interface GroupMembership {
* This may not return all groups the {@link #contains(AccountGroup.UUID)}
* would return {@code true} for, but will at least contain all top level
* groups. This restriction stems from the API of some group systems, which
- * make it expensive to enumate the members of a group.
+ * make it expensive to enumerate the members of a group.
*/
Set<AccountGroup.UUID> getKnownGroups();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 81ff65618c..d448fff15a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -25,11 +25,12 @@ import java.util.Queue;
import java.util.Set;
/**
- * Creates a GroupMembership object from materialized collection of groups.
+ * Creates a GroupMembership checker for the internal group system, which
+ * starts with the seed groups and includes all child groups.
*/
-public class MaterializedGroupMembership implements GroupMembership {
+public class IncludingGroupMembership implements GroupMembership {
public interface Factory {
- MaterializedGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+ IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
}
private final GroupIncludeCache groupIncludeCache;
@@ -37,7 +38,7 @@ public class MaterializedGroupMembership implements GroupMembership {
private final Queue<AccountGroup.UUID> groupQueue;
@Inject
- MaterializedGroupMembership(
+ IncludingGroupMembership(
GroupIncludeCache groupIncludeCache,
@Assisted Iterable<AccountGroup.UUID> seedGroups) {
this.groupIncludeCache = groupIncludeCache;
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
new file mode 100644
index 0000000000..ad65499a5a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -0,0 +1,94 @@
+// 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.account;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
+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.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+
+/**
+ * Implementation of GroupBackend for the internal group system.
+ */
+@Singleton
+public class InternalGroupBackend implements GroupBackend {
+ private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
+ new Function<AccountGroup, GroupReference>() {
+ @Override
+ public GroupReference apply(AccountGroup group) {
+ return GroupReference.forGroup(group);
+ }
+ };
+
+ private final GroupControl.Factory groupControlFactory;
+ private final GroupCache groupCache;
+ private final IncludingGroupMembership.Factory groupMembershipFactory;
+
+
+ @Inject
+ InternalGroupBackend(GroupControl.Factory groupControlFactory,
+ GroupCache groupCache,
+ IncludingGroupMembership.Factory groupMembershipFactory) {
+ this.groupControlFactory = groupControlFactory;
+ this.groupCache = groupCache;
+ this.groupMembershipFactory = groupMembershipFactory;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return AccountGroup.isInternalGroup(uuid);
+ }
+
+ @Override
+ public GroupDescription.Internal get(AccountGroup.UUID uuid) {
+ if (!handles(uuid)) {
+ return null;
+ }
+
+ AccountGroup g = groupCache.get(uuid);
+ if (g == null) {
+ return null;
+ }
+ return GroupDescriptions.forAccountGroup(g);
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(final String name) {
+ Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
+ new Predicate<AccountGroup>() {
+ @Override
+ public boolean apply(AccountGroup group) {
+ // startsWithIgnoreCase && isVisible
+ return group.getName().regionMatches(true, 0, name, 0, name.length())
+ && groupControlFactory.controlFor(group).isVisible();
+ }
+ });
+ return Lists.newArrayList(Iterables.transform(filtered, ACT_GROUP_TO_GROUP_REF));
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ return groupMembershipFactory.create(user.state().getInternalGroups());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
index 237d381c7d..346f406dc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.AccountGroup;
import java.util.Set;
@@ -47,6 +48,6 @@ public class ListGroupMembership implements GroupMembership {
@Override
public Set<AccountGroup.UUID> getKnownGroups() {
- return ImmutableSet.copyOf(groups);
+ return Sets.newHashSet(groups);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 5f94df21e2..1ff1a3e60f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -107,7 +107,10 @@ public class PerformCreateGroup {
final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
group.setVisibleToAll(visibleToAll);
if (ownerGroupId != null) {
- group.setOwnerGroupId(ownerGroupId);
+ AccountGroup ownerGroup = groupCache.get(ownerGroupId);
+ if (ownerGroup != null) {
+ group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
+ }
}
if (groupDescription != null) {
group.setDescription(groupDescription);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
index 6db232a7a2..4c72d49ca0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -60,7 +61,7 @@ public class PerformRenameGroup {
public GroupDetail renameGroup(final String groupName,
final String newGroupName) throws OrmException, NameAlreadyUsedException,
- NoSuchGroupException {
+ NoSuchGroupException, InvalidNameException {
final AccountGroup.NameKey groupNameKey =
new AccountGroup.NameKey(groupName);
final AccountGroup group = groupCache.get(groupNameKey);
@@ -72,12 +73,15 @@ public class PerformRenameGroup {
public GroupDetail renameGroup(final AccountGroup.Id groupId,
final String newName) throws OrmException, NameAlreadyUsedException,
- NoSuchGroupException {
+ NoSuchGroupException, InvalidNameException {
final GroupControl ctl = groupControlFactory.validateFor(groupId);
final AccountGroup group = db.accountGroups().get(groupId);
if (group == null || !ctl.isOwner()) {
throw new NoSuchGroupException(groupId);
}
+ if (newName.trim().isEmpty()) {
+ throw new InvalidNameException();
+ }
final AccountGroup.NameKey old = group.getNameKey();
final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index fc7c0bec91..e44d46e195 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -15,11 +15,8 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import java.util.Set;
-
public interface Realm {
/** Can the end-user modify this field of their own account? */
public boolean allowsEdit(Account.FieldName field);
@@ -29,9 +26,10 @@ public interface Realm {
public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
throws AccountException;
- public void onCreateAccount(AuthRequest who, Account account);
+ public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+ throws AccountException;
- public GroupMembership groups(AccountState who);
+ public void onCreateAccount(AuthRequest who, Account account);
/**
* Locate an account whose local username is the given account name.
@@ -42,9 +40,4 @@ public interface Realm {
* user by that email address.
*/
public Account.Id lookup(String accountName);
-
- /**
- * Search for matching external groups.
- */
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name);
}
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
new file mode 100644
index 0000000000..197496194f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -0,0 +1,161 @@
+// 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.account;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+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.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Universal implementation of the GroupBackend that works with the injected
+ * set of GroupBackends.
+ */
+@Singleton
+public class UniversalGroupBackend implements GroupBackend {
+ private static final Logger log =
+ LoggerFactory.getLogger(UniversalGroupBackend.class);
+
+ private final DynamicSet<GroupBackend> backends;
+
+ @Inject
+ UniversalGroupBackend(DynamicSet<GroupBackend> backends) {
+ this.backends = backends;
+ }
+
+ @Nullable
+ private GroupBackend backend(AccountGroup.UUID uuid) {
+ if (uuid != null) {
+ for (GroupBackend g : backends) {
+ if (g.handles(uuid)) {
+ return g;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return backend(uuid) != null;
+ }
+
+ @Override
+ public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+ GroupBackend b = backend(uuid);
+ if (b == null) {
+ log.warn("Unknown GroupBackend for UUID: " + uuid);
+ return null;
+ }
+ return b.get(uuid);
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(String name) {
+ Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+ for (GroupBackend g : backends) {
+ groups.addAll(g.suggest(name));
+ }
+ return groups;
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ return new UniversalGroupMembership(user);
+ }
+
+ private class UniversalGroupMembership implements GroupMembership {
+ private final Map<GroupBackend, GroupMembership> memberships;
+
+ private UniversalGroupMembership(IdentifiedUser user) {
+ ImmutableMap.Builder<GroupBackend, GroupMembership> builder =
+ ImmutableMap.builder();
+ for (GroupBackend g : backends) {
+ builder.put(g, g.membershipsOf(user));
+ }
+ this.memberships = builder.build();
+ }
+
+ @Nullable
+ private GroupMembership membership(AccountGroup.UUID uuid) {
+ if (uuid != null) {
+ for (Map.Entry<GroupBackend, GroupMembership> m : memberships.entrySet()) {
+ if (m.getKey().handles(uuid)) {
+ return m.getValue();
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean contains(AccountGroup.UUID uuid) {
+ GroupMembership m = membership(uuid);
+ if (m == null) {
+ log.warn("Unknown GroupMembership for UUID: " + uuid);
+ return false;
+ }
+ return m.contains(uuid);
+ }
+
+ @Override
+ public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
+ Multimap<GroupMembership, AccountGroup.UUID> lookups =
+ ArrayListMultimap.create();
+ for (AccountGroup.UUID uuid : uuids) {
+ GroupMembership m = membership(uuid);
+ if (m == null) {
+ log.warn("Unknown GroupMembership for UUID: " + uuid);
+ continue;
+ }
+ lookups.put(m, uuid);
+ }
+ for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
+ lookups.asMap().entrySet()) {
+ if (entry.getKey().containsAnyOf(entry.getValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Set<AccountGroup.UUID> getKnownGroups() {
+ Set<AccountGroup.UUID> groups = Sets.newHashSet();
+ for (GroupMembership m : memberships.values()) {
+ groups.addAll(m.getKnownGroups());
+ }
+ return groups;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
index 3112ed4ec0..d3b2c8392c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
@@ -14,23 +14,22 @@
package com.google.gerrit.server.account;
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.GroupList;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.LinkedList;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Set;
-import java.util.TreeSet;
public class VisibleGroups {
@@ -41,7 +40,6 @@ public class VisibleGroups {
private final Provider<IdentifiedUser> identifiedUser;
private final GroupCache groupCache;
private final GroupControl.Factory groupControlFactory;
- private final GroupDetailFactory.Factory groupDetailFactory;
private boolean onlyVisibleToAll;
private AccountGroup.Type groupType;
@@ -49,12 +47,10 @@ public class VisibleGroups {
@Inject
VisibleGroups(final Provider<IdentifiedUser> currentUser,
final GroupCache groupCache,
- final GroupControl.Factory groupControlFactory,
- final GroupDetailFactory.Factory groupDetailFactory) {
+ final GroupControl.Factory groupControlFactory) {
this.identifiedUser = currentUser;
this.groupCache = groupCache;
this.groupControlFactory = groupControlFactory;
- this.groupDetailFactory = groupDetailFactory;
}
public void setOnlyVisibleToAll(final boolean onlyVisibleToAll) {
@@ -65,15 +61,13 @@ public class VisibleGroups {
this.groupType = groupType;
}
- public GroupList get() throws OrmException, NoSuchGroupException {
- final Iterable<AccountGroup> groups = groupCache.all();
- return createGroupList(filterGroups(groups));
+ public GroupList get() {
+ return createGroupList(filterGroups(groupCache.all()));
}
public GroupList get(final Collection<ProjectControl> projects)
- throws OrmException, NoSuchGroupException {
- final Set<AccountGroup> groups =
- new TreeSet<AccountGroup>(new GroupComparator());
+ throws NoSuchGroupException {
+ Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
for (final ProjectControl projectControl : projects) {
final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
for (final GroupReference groupRef : groupsRefs) {
@@ -81,10 +75,10 @@ public class VisibleGroups {
if (group == null) {
throw new NoSuchGroupException(groupRef.getUUID());
}
- groups.add(group);
+ groups.put(group.getGroupUUID(), group);
}
}
- return createGroupList(filterGroups(groups));
+ return createGroupList(filterGroups(groups.values()));
}
/**
@@ -93,21 +87,18 @@ public class VisibleGroups {
* groups.
* @See GroupMembership#getKnownGroups()
*/
- public GroupList get(final IdentifiedUser user) throws OrmException,
- NoSuchGroupException {
+ public GroupList get(final IdentifiedUser user) throws NoSuchGroupException {
if (identifiedUser.get().getAccountId().equals(user.getAccountId())
|| identifiedUser.get().getCapabilities().canAdministrateServer()) {
- final Set<AccountGroup.UUID> effective =
- user.getEffectiveGroups().getKnownGroups();
- final Set<AccountGroup> groups =
- new TreeSet<AccountGroup>(new GroupComparator());
- for (final AccountGroup.UUID groupId : effective) {
+ Set<AccountGroup.UUID> mine = user.getEffectiveGroups().getKnownGroups();
+ Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+ for (final AccountGroup.UUID groupId : mine) {
AccountGroup group = groupCache.get(groupId);
if (group != null) {
- groups.add(group);
+ groups.put(groupId, group);
}
}
- return createGroupList(filterGroups(groups));
+ return createGroupList(filterGroups(groups.values()));
} else {
throw new NoSuchGroupException("Groups of user '" + user.getAccountId()
+ "' are not visible.");
@@ -115,7 +106,7 @@ public class VisibleGroups {
}
private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
- final List<AccountGroup> filteredGroups = new LinkedList<AccountGroup>();
+ final List<AccountGroup> filteredGroups = Lists.newArrayList();
final boolean isAdmin =
identifiedUser.get().getCapabilities().canAdministrateServer();
for (final AccountGroup group : groups) {
@@ -131,16 +122,12 @@ public class VisibleGroups {
}
filteredGroups.add(group);
}
+ Collections.sort(filteredGroups, new GroupComparator());
return filteredGroups;
}
- private GroupList createGroupList(final List<AccountGroup> groups)
- throws OrmException, NoSuchGroupException {
- final List<GroupDetail> groupDetailList = new ArrayList<GroupDetail>();
- for (final AccountGroup group : groups) {
- groupDetailList.add(groupDetailFactory.create(group.getId()).call());
- }
- return new GroupList(groupDetailList, identifiedUser.get()
+ private GroupList createGroupList(final List<AccountGroup> groups) {
+ return new GroupList(groups, identifiedUser.get()
.getCapabilities().canCreateGroup());
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
index 307a10a834..bf74a4afbd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.GroupCache;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
index 49bf695d81..406ca58b6d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -12,10 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -27,25 +29,25 @@ import org.kohsuke.args4j.spi.Parameters;
import org.kohsuke.args4j.spi.Setter;
public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
@Inject
- public AccountGroupUUIDHandler(final GroupCache groupCache,
+ public AccountGroupUUIDHandler(final GroupBackend groupBackend,
@Assisted final CmdLineParser parser, @Assisted final OptionDef option,
@Assisted final Setter<AccountGroup.UUID> setter) {
super(parser, option, setter);
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
}
@Override
public final int parseArguments(final Parameters params)
throws CmdLineException {
final String n = params.getParameter(0);
- final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+ final GroupReference group = GroupBackends.findBestSuggestion(groupBackend, n);
if (group == null) {
throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
}
- setter.addValue(group.getGroupUUID());
+ setter.addValue(group.getUUID());
return 1;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index d54ae34cd7..8e71b88782 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AuthType;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
new file mode 100644
index 0000000000..9c3d052372
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -0,0 +1,78 @@
+// 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.args4j;
+
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ChangeIdHandler extends OptionHandler<Change.Id> {
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ public ChangeIdHandler(
+ final ReviewDb db,
+ @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+ @Assisted final Setter<Change.Id> setter) {
+ super(parser, option, setter);
+ this.db = db;
+ }
+
+ @Override
+ public final int parseArguments(final Parameters params)
+ throws CmdLineException {
+ final String token = params.getParameter(0);
+ final String[] tokens = token.split(",");
+ if (tokens.length != 3) {
+ throw new CmdLineException(owner, "change should be specified as "
+ + "<project>,<branch>,<change-id>");
+ }
+
+ try {
+ final Change.Key key = Change.Key.parse(tokens[2]);
+ final Project.NameKey project = new Project.NameKey(tokens[0]);
+ final Branch.NameKey branch =
+ new Branch.NameKey(project, "refs/heads/" + tokens[1]);
+ for (final Change change : db.changes().byBranchKey(branch, key)) {
+ setter.addValue(change.getId());
+ return 1;
+ }
+ } catch (IllegalArgumentException e) {
+ throw new CmdLineException(owner, "Change-Id is not valid");
+ } catch (OrmException e) {
+ throw new CmdLineException(owner, "Database error: " + e.getMessage());
+ }
+
+ throw new CmdLineException(owner, "\"" + token + "\": change not found");
+ }
+
+ @Override
+ public final String getDefaultMetaVariable() {
+ return "CHANGE";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
new file mode 100644
index 0000000000..b7f2fb9232
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
@@ -0,0 +1,47 @@
+// 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.args4j;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ObjectIdHandler extends OptionHandler<ObjectId> {
+
+ @Inject
+ public ObjectIdHandler(@Assisted final CmdLineParser parser,
+ @Assisted final OptionDef option, @Assisted final Setter<ObjectId> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public int parseArguments(Parameters params) throws CmdLineException {
+ final String n = params.getParameter(0);
+ setter.addValue(ObjectId.fromString(n));
+ return 1;
+ }
+
+ @Override
+ public String getDefaultMetaVariable() {
+ return "COMMIT";
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
index 2d6a4dfdc7..a48568f9fc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.inject.Inject;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
index e0f7c4c52c..da033e76b2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -71,7 +71,7 @@ public class ProjectControlHandler extends OptionHandler<ProjectControl> {
final ProjectControl control;
try {
Project.NameKey nameKey = new Project.NameKey(projectName);
- control = projectControlFactory.validateFor(nameKey);
+ control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE);
} catch (NoSuchProjectException e) {
throw new CmdLineException(owner, "'" + token + "': not a Gerrit project");
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
index 454a084635..0c20b2d61b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.gerrit.server.util.SocketUtil;
import com.google.inject.Inject;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
index 3df73a8f95..619ec1f6b7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.sshd.args4j;
+package com.google.gerrit.server.args4j;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index e81bfc2328..644f5dff9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -14,15 +14,17 @@
package com.google.gerrit.server.auth.ldap;
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.google.inject.name.Named;
import org.eclipse.jgit.lib.Config;
@@ -47,7 +49,9 @@ import javax.naming.directory.InitialDirContext;
import javax.net.ssl.SSLSocketFactory;
@Singleton class Helper {
- private final GroupCache groupCache;
+ static final String LDAP_UUID = "ldap:";
+
+ private final Cache<String, ImmutableSet<String>> groupsByInclude;
private final Config config;
private final String server;
private final String username;
@@ -58,8 +62,9 @@ import javax.net.ssl.SSLSocketFactory;
private final String readTimeOutMillis;
@Inject
- Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
- this.groupCache = groupCache;
+ Helper(@GerritServerConfig final Config config,
+ @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
+ Cache<String, ImmutableSet<String>> groupsByInclude) {
this.config = config;
this.server = LdapRealm.required(config, "server");
this.username = LdapRealm.optional(config, "username");
@@ -74,6 +79,7 @@ import javax.net.ssl.SSLSocketFactory;
} else {
readTimeOutMillis = null;
}
+ this.groupsByInclude = groupsByInclude;
}
private Properties createContextProperties() {
@@ -195,12 +201,7 @@ import javax.net.ssl.SSLSocketFactory;
final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
for (String dn : groupDNs) {
- for (AccountGroup group : groupCache
- .get(new AccountGroup.ExternalNameKey(dn))) {
- if (group.getType() == AccountGroup.Type.LDAP) {
- actual.add(group.getGroupUUID());
- }
- }
+ actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
}
if (actual.isEmpty()) {
@@ -213,24 +214,31 @@ import javax.net.ssl.SSLSocketFactory;
private void recursivelyExpandGroups(final Set<String> groupDNs,
final LdapSchema schema, final DirContext ctx, final String groupDN) {
if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
- // Recursively identify the groups it is a member of.
- //
- try {
- final Name compositeGroupName = new CompositeName().add(groupDN);
- final Attribute in =
- ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
- if (in != null) {
- final NamingEnumeration<?> groups = in.getAll();
- try {
- while (groups.hasMore()) {
- final String nextDN = (String) groups.next();
- recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
+ ImmutableSet<String> cachedGroupDNs = groupsByInclude.getIfPresent(groupDN);
+ if (cachedGroupDNs == null) {
+ // Recursively identify the groups it is a member of.
+ ImmutableSet.Builder<String> dns = ImmutableSet.builder();
+ try {
+ final Name compositeGroupName = new CompositeName().add(groupDN);
+ final Attribute in =
+ ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
+ if (in != null) {
+ final NamingEnumeration<?> groups = in.getAll();
+ try {
+ while (groups.hasMore()) {
+ dns.add((String) groups.next());
+ }
+ } catch (PartialResultException e) {
}
- } catch (PartialResultException e) {
}
+ } catch (NamingException e) {
+ LdapRealm.log.warn("Could not find group " + groupDN, e);
}
- } catch (NamingException e) {
- LdapRealm.log.warn("Could not find group " + groupDN, e);
+ cachedGroupDNs = dns.build();
+ groupsByInclude.put(groupDN, cachedGroupDNs);
+ }
+ for (String dn : cachedGroupDNs) {
+ recursivelyExpandGroups(groupDNs, schema, ctx, dn);
}
}
}
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
new file mode 100644
index 0000000000..5c30e5c039
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -0,0 +1,227 @@
+// 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.auth.ldap;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+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.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+/**
+ * Implementation of GroupBackend for the LDAP group system.
+ */
+public class LdapGroupBackend implements GroupBackend {
+ private static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+
+ private static final String LDAP_NAME = "ldap/";
+ private static final String GROUPNAME = "groupname";
+
+ private final Helper helper;
+ private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
+ private final LoadingCache<String, Boolean> existsCache;
+ private final Provider<CurrentUser> userProvider;
+
+ @Inject
+ LdapGroupBackend(
+ Helper helper,
+ @Named(GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+ @Named(GROUP_EXIST_CACHE) LoadingCache<String, Boolean> existsCache,
+ Provider<CurrentUser> userProvider) {
+ this.helper = helper;
+ this.membershipCache = membershipCache;
+ this.existsCache = existsCache;
+ this.userProvider = userProvider;
+ }
+
+ private static boolean isLdapUUID(AccountGroup.UUID uuid) {
+ return uuid.get().startsWith(LDAP_UUID);
+ }
+
+ private static GroupReference groupReference(LdapQuery.Result res)
+ throws NamingException {
+ return new GroupReference(
+ new AccountGroup.UUID(LDAP_UUID + res.getDN()),
+ LDAP_NAME + cnFor(res.getDN()));
+ }
+
+ private static String cnFor(String dn) {
+ try {
+ LdapName name = new LdapName(dn);
+ if (!name.isEmpty()) {
+ String cn = name.get(name.size() - 1);
+ int index = cn.indexOf('=');
+ if (index >= 0) {
+ cn = cn.substring(index + 1);
+ }
+ return cn;
+ }
+ } catch (InvalidNameException e) {
+ log.warn("Cannot parse LDAP dn for cn", e);
+ }
+ return dn;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return isLdapUUID(uuid);
+ }
+
+ @Override
+ public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
+ if (!handles(uuid)) {
+ return null;
+ }
+
+ String groupDn = uuid.get().substring(LDAP_UUID.length());
+ CurrentUser user = userProvider.get();
+ if (!(user instanceof IdentifiedUser)
+ || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
+ try {
+ if (!existsCache.get(groupDn)) {
+ return null;
+ }
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s in LDAP", groupDn), e);
+ return null;
+ }
+ }
+
+ final String name = LDAP_NAME + cnFor(groupDn);
+ return new GroupDescription.Basic() {
+ @Override
+ public AccountGroup.UUID getGroupUUID() {
+ return uuid;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isVisibleToAll() {
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(String name) {
+ AccountGroup.UUID uuid = new AccountGroup.UUID(name);
+ if (isLdapUUID(uuid)) {
+ GroupDescription.Basic g = get(uuid);
+ if (g == null) {
+ return Collections.emptySet();
+ }
+ return Collections.singleton(GroupReference.forGroup(g));
+ } else if (name.startsWith(LDAP_NAME)) {
+ return suggestLdap(name.substring(LDAP_NAME.length()));
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ String id = findId(user.state().getExternalIds());
+ if (id == null) {
+ return GroupMembership.EMPTY;
+ }
+
+ try {
+ return new ListGroupMembership(membershipCache.get(id));
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
+ return GroupMembership.EMPTY;
+ }
+ }
+
+ private static String findId(final Collection<AccountExternalId> ids) {
+ for (final AccountExternalId i : ids) {
+ if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
+ return i.getSchemeRest();
+ }
+ }
+ return null;
+ }
+
+
+ private Set<GroupReference> suggestLdap(String name) {
+ if (name.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ Set<GroupReference> out = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+ try {
+ DirContext ctx = helper.open();
+ try {
+ // Do exact lookups until there are at least 3 characters.
+ name = Rdn.escapeValue(name) + ((name.length() >= 3) ? "*" : "");
+ LdapSchema schema = helper.getSchema(ctx);
+ ParameterizedString filter = ParameterizedString.asis(
+ schema.groupPattern.replace(GROUPNAME, name).toString());
+ Set<String> returnAttrs = Collections.<String>emptySet();
+ Map<String, String> params = Collections.emptyMap();
+ for (String groupBase : schema.groupBases) {
+ LdapQuery query = new LdapQuery(
+ groupBase, schema.groupScope, filter, returnAttrs);
+ for (LdapQuery.Result res : query.query(ctx, params)) {
+ out.add(groupReference(res));
+ }
+ }
+ } finally {
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ log.warn("Cannot close LDAP query handle", e);
+ }
+ }
+ } catch (NamingException e) {
+ log.warn("Cannot query LDAP for groups matching requested name", e);
+ }
+ return out;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 6eb2f5460a..f1b15f9bbc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,10 +16,13 @@ package com.google.gerrit.server.auth.ldap;
import static java.util.concurrent.TimeUnit.HOURS;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
@@ -29,20 +32,37 @@ import java.util.Set;
public class LdapModule extends CacheModule {
static final String USERNAME_CACHE = "ldap_usernames";
static final String GROUP_CACHE = "ldap_groups";
+ static final String GROUP_EXIST_CACHE = "ldap_group_existence";
+ static final String GROUPS_BYINCLUDE_CACHE = "ldap_groups_byinclude";
+
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Set<AccountGroup.UUID>>> groups =
- new TypeLiteral<Cache<String, Set<AccountGroup.UUID>>>() {};
- core(groups, GROUP_CACHE).maxAge(1, HOURS) //
- .populateWith(LdapRealm.MemberLoader.class);
+ cache(GROUP_CACHE,
+ String.class,
+ new TypeLiteral<Set<AccountGroup.UUID>>() {})
+ .expireAfterWrite(1, HOURS)
+ .loader(LdapRealm.MemberLoader.class);
+
+ cache(USERNAME_CACHE,
+ String.class,
+ new TypeLiteral<Optional<Account.Id>>() {})
+ .loader(LdapRealm.UserLoader.class);
- final TypeLiteral<Cache<String, Account.Id>> usernames =
- new TypeLiteral<Cache<String, Account.Id>>() {};
- core(usernames, USERNAME_CACHE) //
- .populateWith(LdapRealm.UserLoader.class);
+ cache(GROUP_EXIST_CACHE,
+ String.class,
+ new TypeLiteral<Boolean>() {})
+ .expireAfterWrite(1, HOURS)
+ .loader(LdapRealm.ExistenceLoader.class);
+
+ cache(GROUPS_BYINCLUDE_CACHE,
+ String.class,
+ new TypeLiteral<ImmutableSet<String>>() {})
+ .expireAfterWrite(1, HOURS);
bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
bind(Helper.class);
+
+ DynamicSet.bind(binder(), GroupBackend.class).to(LdapGroupBackend.class);
}
}
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 e085d1efc2..72eb7ec0b1 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
@@ -16,7 +16,10 @@ package com.google.gerrit.server.auth.ldap;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
-import com.google.common.collect.Iterables;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -24,20 +27,13 @@ import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -48,15 +44,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.naming.CompositeName;
+import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
@@ -65,34 +62,30 @@ class LdapRealm implements Realm {
static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
static final String USERNAME = "username";
- private static final String GROUPNAME = "groupname";
private final Helper helper;
private final AuthConfig authConfig;
private final EmailExpander emailExpander;
- private final Cache<String, Account.Id> usernameCache;
+ private final LoadingCache<String, Optional<Account.Id>> usernameCache;
private final Set<Account.FieldName> readOnlyAccountFields;
private final Config config;
- private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
@Inject
LdapRealm(
final Helper helper,
final AuthConfig authConfig,
final EmailExpander emailExpander,
- @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.UUID>> membershipCache,
- @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
- @GerritServerConfig final Config config,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+ @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache,
+ @GerritServerConfig final Config config) {
this.helper = helper;
this.authConfig = authConfig;
this.emailExpander = emailExpander;
this.usernameCache = usernameCache;
this.membershipCache = membershipCache;
this.config = config;
- this.groupMembershipFactory = groupMembershipFactory;
this.readOnlyAccountFields = new HashSet<Account.FieldName>();
@@ -189,6 +182,7 @@ class LdapRealm implements Realm {
return r.isEmpty() ? null : r;
}
+ @Override
public AuthRequest authenticate(final AuthRequest who)
throws AccountException {
if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
@@ -255,66 +249,30 @@ class LdapRealm implements Realm {
}
@Override
- public void onCreateAccount(final AuthRequest who, final Account account) {
- usernameCache.put(who.getLocalUser(), account.getId());
- }
-
- @Override
- public GroupMembership groups(final AccountState who) {
- return groupMembershipFactory.create(Iterables.concat(
- membershipCache.get(findId(who.getExternalIds())),
- who.getInternalGroups()));
- }
-
- private static String findId(final Collection<AccountExternalId> ids) {
- for (final AccountExternalId i : ids) {
- if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
- return i.getSchemeRest();
- }
- }
- return null;
+ public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+ return who;
}
@Override
- public Account.Id lookup(final String accountName) {
- return usernameCache.get(accountName);
+ public void onCreateAccount(final AuthRequest who, final Account account) {
+ usernameCache.put(who.getLocalUser(), Optional.of(account.getId()));
}
@Override
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
- final Set<AccountGroup.ExternalNameKey> out;
- final Map<String, String> params = Collections.<String, String> emptyMap();
-
- out = new HashSet<AccountGroup.ExternalNameKey>();
+ public Account.Id lookup(String accountName) {
+ if (Strings.isNullOrEmpty(accountName)) {
+ return null;
+ }
try {
- final DirContext ctx = helper.open();
- try {
- final LdapSchema schema = helper.getSchema(ctx);
- final ParameterizedString filter =
- ParameterizedString.asis(schema.groupPattern
- .replace(GROUPNAME, name).toString());
- for (String groupBase : schema.groupBases) {
- final LdapQuery query =
- new LdapQuery(groupBase, schema.groupScope, filter, Collections
- .<String> emptySet());
- for (LdapQuery.Result res : query.query(ctx, params)) {
- out.add(new AccountGroup.ExternalNameKey(res.getDN()));
- }
- }
- } finally {
- try {
- ctx.close();
- } catch (NamingException e) {
- log.warn("Cannot close LDAP query handle", e);
- }
- }
- } catch (NamingException e) {
- log.warn("Cannot query LDAP for groups matching requested name", e);
+ Optional<Account.Id> id = usernameCache.get(accountName);
+ return id != null ? id.orNull() : null;
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e);
+ return null;
}
- return out;
}
- static class UserLoader extends EntryCreator<String, Account.Id> {
+ static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -323,25 +281,23 @@ class LdapRealm implements Realm {
}
@Override
- public Account.Id createEntry(final String username) throws Exception {
+ public Optional<Account.Id> load(String username) throws Exception {
+ final ReviewDb db = schema.open();
try {
- final ReviewDb db = schema.open();
- try {
- final AccountExternalId extId =
- db.accountExternalIds().get(
- new AccountExternalId.Key(SCHEME_GERRIT, username));
- return extId != null ? extId.getAccountId() : null;
- } finally {
- db.close();
+ final AccountExternalId extId =
+ db.accountExternalIds().get(
+ new AccountExternalId.Key(SCHEME_GERRIT, username));
+ if (extId != null) {
+ return Optional.of(extId.getAccountId());
}
- } catch (OrmException e) {
- log.warn("Cannot query for username in database", e);
- return null;
+ return Optional.absent();
+ } finally {
+ db.close();
}
}
}
- static class MemberLoader extends EntryCreator<String, Set<AccountGroup.UUID>> {
+ static class MemberLoader extends CacheLoader<String, Set<AccountGroup.UUID>> {
private final Helper helper;
@Inject
@@ -350,8 +306,7 @@ class LdapRealm implements Realm {
}
@Override
- public Set<AccountGroup.UUID> createEntry(final String username)
- throws Exception {
+ public Set<AccountGroup.UUID> load(String username) throws Exception {
final DirContext ctx = helper.open();
try {
return helper.queryForGroups(ctx, username, null);
@@ -363,10 +318,34 @@ class LdapRealm implements Realm {
}
}
}
+ }
+
+ static class ExistenceLoader extends CacheLoader<String, Boolean> {
+ private final Helper helper;
+
+ @Inject
+ ExistenceLoader(final Helper helper) {
+ this.helper = helper;
+ }
@Override
- public Set<AccountGroup.UUID> missing(final String key) {
- return Collections.emptySet();
+ public Boolean load(final String groupDn) throws Exception {
+ final DirContext ctx = helper.open();
+ try {
+ Name compositeGroupName = new CompositeName().add(groupDn);
+ try {
+ ctx.getAttributes(compositeGroupName);
+ return true;
+ } catch (NamingException e) {
+ return false;
+ }
+ } finally {
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ log.warn("Cannot close LDAP query handle", e);
+ }
+ }
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
deleted file mode 100644
index 7892ea1e06..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.gerrit.server.cache;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-public interface Cache<K, V> {
- /** Get the element from the cache, or null if not stored in the cache. */
- public V get(K key);
-
- /** Put one element into the cache, replacing any existing value. */
- public void put(K key, V value);
-
- /** Remove any existing value from the cache, no-op if not present. */
- public void remove(K key);
-
- /** Remove all cached items. */
- public void removeAll();
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
new file mode 100644
index 0000000000..625bd14787
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -0,0 +1,46 @@
+// 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.gerrit.server.cache;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.inject.TypeLiteral;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+/** Configure a cache declared within a {@link CacheModule} instance. */
+public interface CacheBinding<K, V> {
+ /** Set the total size of the cache. */
+ CacheBinding<K, V> maximumWeight(long weight);
+
+ /** Set the time an element lives before being expired. */
+ CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
+
+ /** Populate the cache with items from the CacheLoader. */
+ CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
+
+ /** Algorithm to weigh an object with a method other than the unit weight 1. */
+ CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
+
+ String name();
+ TypeLiteral<K> keyType();
+ TypeLiteral<V> valueType();
+ long maximumWeight();
+ @Nullable Long expireAfterWrite(TimeUnit unit);
+ @Nullable Weigher<K, V> weigher();
+ @Nullable CacheLoader<K, V> loader();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 7fb3b3b591..c1e92dab29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -14,33 +14,41 @@
package com.google.gerrit.server.cache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.Exports;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
-import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.name.Names;
+import com.google.inject.util.Types;
import java.io.Serializable;
+import java.lang.reflect.Type;
/**
* Miniature DSL to support binding {@link Cache} instances in Guice.
*/
public abstract class CacheModule extends AbstractModule {
+ private static final TypeLiteral<Cache<?, ?>> ANY_CACHE =
+ new TypeLiteral<Cache<?, ?>>() {};
+
/**
- * Declare an unnamed in-memory cache.
+ * Declare a named in-memory cache.
*
* @param <K> type of key used to lookup entries.
* @param <V> type of value stored by the cache.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites.
- * @return binding to describe the cache. Caller must set at least the name on
- * the returned binding.
+ * @return binding to describe the cache.
*/
- protected <K, V> UnnamedCacheBinding<K, V> core(
- final TypeLiteral<Cache<K, V>> type) {
- return core(Key.get(type));
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ Class<K> keyType,
+ Class<V> valType) {
+ return cache(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
/**
@@ -48,74 +56,127 @@ public abstract class CacheModule extends AbstractModule {
*
* @param <K> type of key used to lookup entries.
* @param <V> type of value stored by the cache.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
* @return binding to describe the cache.
*/
- protected <K, V> NamedCacheBinding<K, V> core(
- final TypeLiteral<Cache<K, V>> type, final String name) {
- return core(Key.get(type, Names.named(name))).name(name);
- }
-
- private <K, V> UnnamedCacheBinding<K, V> core(final Key<Cache<K, V>> key) {
- final boolean disk = false;
- final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
- bind(key).toProvider(b).in(Scopes.SINGLETON);
- return b;
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ Class<K> keyType,
+ TypeLiteral<V> valType) {
+ return cache(name, TypeLiteral.get(keyType), valType);
}
/**
- * Declare an unnamed in-memory/on-disk cache.
+ * Declare a named in-memory cache.
*
- * @param <K> type of key used to find entries, must be {@link Serializable}.
- * @param <V> type of value stored by the cache, must be {@link Serializable}.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
- * @return binding to describe the cache. Caller must set at least the name on
- * the returned binding.
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
*/
- protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding<K, V> disk(
- final TypeLiteral<Cache<K, V>> type) {
- return disk(Key.get(type));
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
+ Type type = Types.newParameterizedType(
+ Cache.class,
+ keyType.getType(), valType.getType());
+
+ @SuppressWarnings("unchecked")
+ Key<Cache<K, V>> key = (Key<Cache<K, V>>) Key.get(type, Names.named(name));
+
+ CacheProvider<K, V> m =
+ new CacheProvider<K, V>(this, name, keyType, valType);
+ bind(key).toProvider(m).in(Scopes.SINGLETON);
+ bind(ANY_CACHE).annotatedWith(Exports.named(name)).to(key);
+ return m.maximumWeight(1024);
+ }
+
+ <K,V> Provider<CacheLoader<K,V>> bindCacheLoader(
+ CacheProvider<K, V> m,
+ Class<? extends CacheLoader<K,V>> impl) {
+ Type type = Types.newParameterizedType(
+ Cache.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ Type loadingType = Types.newParameterizedType(
+ LoadingCache.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ Type loaderType = Types.newParameterizedType(
+ CacheLoader.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ @SuppressWarnings("unchecked")
+ Key<LoadingCache<K, V>> key =
+ (Key<LoadingCache<K, V>>) Key.get(type, Names.named(m.name));
+
+ @SuppressWarnings("unchecked")
+ Key<LoadingCache<K, V>> loadingKey =
+ (Key<LoadingCache<K, V>>) Key.get(loadingType, Names.named(m.name));
+
+ @SuppressWarnings("unchecked")
+ Key<CacheLoader<K, V>> loaderKey =
+ (Key<CacheLoader<K, V>>) Key.get(loaderType, Names.named(m.name));
+
+ bind(loaderKey).to(impl).in(Scopes.SINGLETON);
+ bind(loadingKey).to(key);
+ return getProvider(loaderKey);
+ }
+
+ <K,V> Provider<Weigher<K,V>> bindWeigher(
+ CacheProvider<K, V> m,
+ Class<? extends Weigher<K,V>> impl) {
+ Type weigherType = Types.newParameterizedType(
+ Weigher.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ @SuppressWarnings("unchecked")
+ Key<Weigher<K, V>> key =
+ (Key<Weigher<K, V>>) Key.get(weigherType, Names.named(m.name));
+
+ bind(key).to(impl).in(Scopes.SINGLETON);
+ return getProvider(key);
}
/**
* Declare a named in-memory/on-disk cache.
*
- * @param <K> type of key used to find entries, must be {@link Serializable}.
- * @param <V> type of value stored by the cache, must be {@link Serializable}.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
* @return binding to describe the cache.
*/
- protected <K extends Serializable, V extends Serializable> NamedCacheBinding<K, V> disk(
- final TypeLiteral<Cache<K, V>> type, final String name) {
- return disk(Key.get(type, Names.named(name))).name(name);
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ Class<K> keyType,
+ Class<V> valType) {
+ return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
- private <K, V> UnnamedCacheBinding<K, V> disk(final Key<Cache<K, V>> key) {
- final boolean disk = true;
- final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
- bind(key).toProvider(b).in(Scopes.SINGLETON);
- return b;
- }
-
- <K, V> Provider<EntryCreator<K, V>> getEntryCreator(CacheProvider<K, V> cp,
- Class<? extends EntryCreator<K, V>> type) {
- Key<EntryCreator<K, V>> key = newKey();
- bind(key).to(type).in(Scopes.SINGLETON);
- return getProvider(key);
- }
-
- @SuppressWarnings("unchecked")
- private static <K, V> Key<EntryCreator<K, V>> newKey() {
- return (Key<EntryCreator<K, V>>) newKeyImpl();
+ /**
+ * Declare a named in-memory/on-disk cache.
+ *
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
+ */
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ Class<K> keyType,
+ TypeLiteral<V> valType) {
+ return persist(name, TypeLiteral.get(keyType), valType);
}
- private static Key<?> newKeyImpl() {
- return Key.get(EntryCreator.class, UniqueAnnotations.create());
+ /**
+ * Declare a named in-memory/on-disk cache.
+ *
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
+ */
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
+ return ((CacheProvider<K, V>) cache(name, keyType, valType))
+ .persist(true);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 1fa047bc35..1b8eea5124 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,130 +14,156 @@
package com.google.gerrit.server.cache;
-import static com.google.gerrit.server.cache.EvictionPolicy.LFU;
-import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
import java.util.concurrent.TimeUnit;
-public final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
- NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
+import javax.annotation.Nullable;
+
+class CacheProvider<K, V>
+ implements Provider<Cache<K, V>>,
+ CacheBinding<K, V> {
private final CacheModule module;
- private final boolean disk;
- private int memoryLimit;
- private int diskLimit;
- private long maxAge;
- private EvictionPolicy evictionPolicy;
- private String cacheName;
- private ProxyCache<K, V> cache;
- private Provider<EntryCreator<K, V>> entryCreator;
-
- CacheProvider(final boolean disk, CacheModule module) {
- this.disk = disk;
+ final String name;
+ private final TypeLiteral<K> keyType;
+ private final TypeLiteral<V> valType;
+ private boolean persist;
+ private long maximumWeight;
+ private Long expireAfterWrite;
+ private Provider<CacheLoader<K, V>> loader;
+ private Provider<Weigher<K, V>> weigher;
+
+ private String plugin;
+ private MemoryCacheFactory memoryCacheFactory;
+ private PersistentCacheFactory persistentCacheFactory;
+ private boolean frozen;
+
+ CacheProvider(CacheModule module,
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
this.module = module;
-
- memoryLimit(1024);
- maxAge(90, DAYS);
- evictionPolicy(LFU);
-
- if (disk) {
- diskLimit(16384);
- }
+ this.name = name;
+ this.keyType = keyType;
+ this.valType = valType;
}
- @Inject
- void setCachePool(final CachePool pool) {
- this.cache = pool.register(this);
+ @Inject(optional = true)
+ void setPluginName(@PluginName String pluginName) {
+ this.plugin = pluginName;
}
- public void bind(Cache<K, V> impl) {
- if (cache == null) {
- throw new ProvisionException("Cache was never registered");
- }
- cache.bind(impl);
+ @Inject
+ void setMemoryCacheFactory(MemoryCacheFactory factory) {
+ this.memoryCacheFactory = factory;
}
- public EntryCreator<K, V> getEntryCreator() {
- return entryCreator != null ? entryCreator.get() : null;
+ @Inject(optional = true)
+ void setPersistentCacheFactory(@Nullable PersistentCacheFactory factory) {
+ this.persistentCacheFactory = factory;
}
- public String getName() {
- if (cacheName == null) {
- throw new ProvisionException("Cache has no name");
- }
- return cacheName;
+ CacheBinding<K, V> persist(boolean p) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ persist = p;
+ return this;
}
- public boolean disk() {
- return disk;
+ @Override
+ public CacheBinding<K, V> maximumWeight(long weight) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ maximumWeight = weight;
+ return this;
}
- public int memoryLimit() {
- return memoryLimit;
+ @Override
+ public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ expireAfterWrite = SECONDS.convert(duration, unit);
+ return this;
}
- public int diskLimit() {
- return diskLimit;
+ @Override
+ public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ loader = module.bindCacheLoader(this, impl);
+ return this;
}
- public long maxAge() {
- return maxAge;
+ @Override
+ public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> impl) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ weigher = module.bindWeigher(this, impl);
+ return this;
}
- public EvictionPolicy evictionPolicy() {
- return evictionPolicy;
+ @Override
+ public String name() {
+ if (!Strings.isNullOrEmpty(plugin)) {
+ return plugin + "." + name;
+ }
+ return name;
}
- public NamedCacheBinding<K, V> name(final String name) {
- if (cacheName != null) {
- throw new IllegalStateException("Cache name already set");
- }
- cacheName = name;
- return this;
+ @Override
+ public TypeLiteral<K> keyType() {
+ return keyType;
}
- public NamedCacheBinding<K, V> memoryLimit(final int objects) {
- memoryLimit = objects;
- return this;
+ @Override
+ public TypeLiteral<V> valueType() {
+ return valType;
}
- public NamedCacheBinding<K, V> diskLimit(final int objects) {
- if (!disk) {
- // TODO This should really be a compile time type error, but I'm
- // too lazy to create the mess of permutations required to setup
- // type safe returns for bindings in our little DSL.
- //
- throw new IllegalStateException("Cache is not disk based");
- }
- diskLimit = objects;
- return this;
+ @Override
+ public long maximumWeight() {
+ return maximumWeight;
}
- public NamedCacheBinding<K, V> maxAge(final long duration, final TimeUnit unit) {
- maxAge = SECONDS.convert(duration, unit);
- return this;
+ @Override
+ @Nullable
+ public Long expireAfterWrite(TimeUnit unit) {
+ return expireAfterWrite != null
+ ? unit.convert(expireAfterWrite, SECONDS)
+ : null;
}
@Override
- public NamedCacheBinding<K, V> evictionPolicy(final EvictionPolicy policy) {
- evictionPolicy = policy;
- return this;
+ @Nullable
+ public Weigher<K, V> weigher() {
+ return weigher != null ? weigher.get() : null;
}
- public NamedCacheBinding<K, V> populateWith(
- Class<? extends EntryCreator<K, V>> creator) {
- entryCreator = module.getEntryCreator(this, creator);
- return this;
+ @Override
+ @Nullable
+ public CacheLoader<K, V> loader() {
+ return loader != null ? loader.get() : null;
}
+ @Override
public Cache<K, V> get() {
- if (cache == null) {
- throw new ProvisionException("Cache \"" + cacheName + "\" not available");
+ frozen = true;
+
+ if (loader != null) {
+ CacheLoader<K, V> ldr = loader.get();
+ if (persist && persistentCacheFactory != null) {
+ return persistentCacheFactory.build(this, ldr);
+ }
+ return memoryCacheFactory.build(this, ldr);
+ } else if (persist && persistentCacheFactory != null) {
+ return persistentCacheFactory.build(this);
+ } else {
+ return memoryCacheFactory.build(this);
}
- return cache;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
index cff4f111b5..078f2dc30c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,11 +14,10 @@
package com.google.gerrit.server.cache;
-/** How entries should be evicted from the cache. */
-public enum EvictionPolicy {
- /** Least recently used is evicted first. */
- LRU,
+import com.google.common.cache.RemovalNotification;
- /** Least frequently used is evicted first. */
- LFU;
-}
+public interface CacheRemovalListener<K,V> {
+ public void onRemoval(String pluginName,
+ String cacheName,
+ RemovalNotification<K, V> notification);
+} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
deleted file mode 100644
index bafdc49836..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2011 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.cache;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * An infinitely sized cache backed by java.util.ConcurrentHashMap.
- * <p>
- * This cache type is only suitable for unit tests, as it has no upper limit on
- * number of items held in the cache. No upper limit can result in memory leaks
- * in production servers.
- */
-public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
- private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
-
- @Override
- public V get(K key) {
- return map.get(key);
- }
-
- @Override
- public void put(K key, V value) {
- map.put(key, value);
- }
-
- @Override
- public void remove(K key) {
- map.remove(key);
- }
-
- @Override
- public void removeAll() {
- map.clear();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
deleted file mode 100644
index af07e08636..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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.gerrit.server.cache;
-
-/**
- * Creates a cache entry on demand when its not found.
- *
- * @param <K> type of the cache's key.
- * @param <V> type of the cache's value element.
- */
-public abstract class EntryCreator<K, V> {
- /**
- * Invoked on a cache miss, to compute the cache entry.
- *
- * @param key entry whose content needs to be obtained.
- * @return new cache content. The caller will automatically put this object
- * into the cache.
- * @throws Exception the cache content cannot be computed. No entry will be
- * stored in the cache, and {@link #missing(Object)} will be invoked
- * instead. Future requests for the same key will retry this method.
- */
- public abstract V createEntry(K key) throws Exception;
-
- /** Invoked when {@link #createEntry(Object)} fails, by default return null. */
- public V missing(K key) {
- return null;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
new file mode 100644
index 0000000000..c98ddf943c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
@@ -0,0 +1,60 @@
+// 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.cache;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * This listener dispatches removal events to all other RemovalListeners
+ * attached via the DynamicSet API.
+ *
+ * @param <K>
+ * @param <V>
+ */
+@SuppressWarnings("rawtypes")
+public class ForwardingRemovalListener<K, V> implements RemovalListener<K, V> {
+ public interface Factory {
+ ForwardingRemovalListener create(String cacheName);
+ }
+
+ private final DynamicSet<CacheRemovalListener> listeners;
+ private final String cacheName;
+ private String pluginName = "gerrit";
+
+ @Inject
+ ForwardingRemovalListener(DynamicSet<CacheRemovalListener> listeners,
+ @Assisted String cacheName) {
+ this.listeners = listeners;
+ this.cacheName = cacheName;
+ }
+
+ @Inject(optional = true)
+ void setPluginName(String name) {
+ if (!Strings.isNullOrEmpty(name)) {
+ this.pluginName = name;
+ }
+ }
+
+ public void onRemoval(RemovalNotification<K, V> notification) {
+ for (CacheRemovalListener<K, V> l : listeners) {
+ l.onRemoval(pluginName, cacheName, notification);
+ }
+ }
+} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
new file mode 100644
index 0000000000..6b8b489611
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
@@ -0,0 +1,27 @@
+// 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.cache;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+public interface MemoryCacheFactory {
+ <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+ <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
deleted file mode 100644
index 3394c71d05..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.gerrit.server.cache;
-
-import java.util.concurrent.TimeUnit;
-
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface NamedCacheBinding<K, V> {
- /** Set the number of objects to cache in memory. */
- public NamedCacheBinding<K, V> memoryLimit(int objects);
-
- /** Set the number of objects to cache in memory. */
- public NamedCacheBinding<K, V> diskLimit(int objects);
-
- /** Set the time an element lives before being expired. */
- public NamedCacheBinding<K, V> maxAge(long duration, TimeUnit durationUnits);
-
- /** Set the eviction policy for elements when the cache is full. */
- public NamedCacheBinding<K, V> evictionPolicy(EvictionPolicy policy);
-
- /** Populate the cache with items from the EntryCreator. */
- public NamedCacheBinding<K, V> populateWith(Class<? extends EntryCreator<K, V>> creator);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
new file mode 100644
index 0000000000..983e956be5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -0,0 +1,27 @@
+// 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.cache;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+public interface PersistentCacheFactory {
+ <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+ <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
index 1fac8c5606..dca4a83e56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -31,15 +31,13 @@ import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
import java.util.concurrent.Callable;
-public class AbandonChange implements Callable<ReviewResult> {
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
- public interface Factory {
- AbandonChange create(PatchSet.Id patchSetId, String changeComment);
- }
+public class AbandonChange implements Callable<ReviewResult> {
private final AbandonedSender.Factory abandonedSenderFactory;
private final ChangeControl.Factory changeControlFactory;
@@ -47,33 +45,48 @@ public class AbandonChange implements Callable<ReviewResult> {
private final IdentifiedUser currentUser;
private final ChangeHooks hooks;
- private final PatchSet.Id patchSetId;
- private final String changeComment;
+ @Argument(index = 0, required = true, multiValued = false, usage = "change to abandon")
+ private Change.Id changeId;
+
+ public void setChangeId(final Change.Id changeId) {
+ this.changeId = changeId;
+ }
+
+ @Option(name = "--message", aliases = {"-m"},
+ usage = "optional message to append to change")
+ private String message;
+
+ public void setMessage(final String message) {
+ this.message = message;
+ }
@Inject
AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
final ChangeControl.Factory changeControlFactory, final ReviewDb db,
- final IdentifiedUser currentUser, final ChangeHooks hooks,
- @Assisted final PatchSet.Id patchSetId,
- @Assisted final String changeComment) {
+ final IdentifiedUser currentUser, final ChangeHooks hooks) {
this.abandonedSenderFactory = abandonedSenderFactory;
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
this.hooks = hooks;
- this.patchSetId = patchSetId;
- this.changeComment = changeComment;
+ changeId = null;
+ message = null;
}
@Override
public ReviewResult call() throws EmailException,
InvalidChangeOperationException, NoSuchChangeException, OrmException {
- final ReviewResult result = new ReviewResult();
+ if (changeId == null) {
+ throw new InvalidChangeOperationException("changeId is required");
+ }
- final Change.Id changeId = patchSetId.getParentKey();
+ final ReviewResult result = new ReviewResult();
result.setChangeId(changeId);
+
final ChangeControl control = changeControlFactory.validateFor(changeId);
+ final Change change = db.changes().get(changeId);
+ final PatchSet.Id patchSetId = change.currentPatchSetId();
final PatchSet patch = db.patchSets().get(patchSetId);
if (!control.canAbandon()) {
result.addError(new ReviewResult.Error(
@@ -88,9 +101,9 @@ public class AbandonChange implements Callable<ReviewResult> {
currentUser.getAccountId(), patchSetId);
final StringBuilder msgBuf =
new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
- if (changeComment != null && changeComment.length() > 0) {
+ if (message != null && message.length() > 0) {
msgBuf.append("\n\n");
- msgBuf.append(changeComment);
+ msgBuf.append(message);
}
cmsg.setMessage(msgBuf.toString());
@@ -99,8 +112,7 @@ public class AbandonChange implements Callable<ReviewResult> {
new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
- if (change.getStatus().isOpen()
- && change.currentPatchSetId().equals(patchSetId)) {
+ if (change.getStatus().isOpen()) {
change.setStatus(Change.Status.ABANDONED);
ChangeUtil.updated(change);
return change;
@@ -109,11 +121,17 @@ public class AbandonChange implements Callable<ReviewResult> {
}
}
});
- ChangeUtil.updatedChange(
- db, currentUser, updatedChange, cmsg, abandonedSenderFactory,
- "Change is no longer open or patchset is not latest");
+
+ if (updatedChange == null) {
+ result.addError(new ReviewResult.Error(
+ ReviewResult.Error.Type.CHANGE_IS_CLOSED));
+ return result;
+ }
+
+ ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+ abandonedSenderFactory);
hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
- changeComment, db);
+ message, db);
}
return result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index 268e11861b..f466231323 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -20,8 +20,8 @@ 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.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -44,7 +44,7 @@ public class DeleteDraftPatchSet implements Callable<ReviewResult> {
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSetInfoFactory patchSetInfoFactory;
private final PatchSet.Id patchSetId;
@@ -52,7 +52,7 @@ public class DeleteDraftPatchSet implements Callable<ReviewResult> {
@Inject
DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
ReviewDb db, GitRepositoryManager gitManager,
- ReplicationQueue replication, PatchSetInfoFactory patchSetInfoFactory,
+ GitReferenceUpdated replication, PatchSetInfoFactory patchSetInfoFactory,
@Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.db = db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index 028feac968..a71e12e7b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.changedetail;
+import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.ReviewResult;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -37,14 +38,17 @@ public class PublishDraft implements Callable<ReviewResult> {
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
+ private final ChangeHooks hooks;
private final PatchSet.Id patchSetId;
@Inject
PublishDraft(ChangeControl.Factory changeControlFactory,
- ReviewDb db, @Assisted final PatchSet.Id patchSetId) {
+ ReviewDb db, @Assisted final PatchSet.Id patchSetId,
+ final ChangeHooks hooks) {
this.changeControlFactory = changeControlFactory;
this.db = db;
+ this.hooks = hooks;
this.patchSetId = patchSetId;
}
@@ -70,31 +74,29 @@ public class PublishDraft implements Callable<ReviewResult> {
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.PUBLISH_NOT_PERMITTED));
} else {
- db.patchSets().atomicUpdate(patchSetId, new AtomicUpdate<PatchSet>() {
+ final PatchSet updatedPatch = db.patchSets().atomicUpdate(patchSetId,
+ new AtomicUpdate<PatchSet>() {
@Override
public PatchSet update(PatchSet patchset) {
- if (patchset.isDraft()) {
- patchset.setDraft(false);
- }
- return null;
+ patchset.setDraft(false);
+ return patchset;
}
});
- final Change change = db.changes().get(changeId);
- if (change.getStatus() == Change.Status.DRAFT) {
- db.changes().atomicUpdate(changeId,
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus() == Change.Status.DRAFT) {
- change.setStatus(Change.Status.NEW);
- ChangeUtil.updated(change);
- return change;
- } else {
- return null;
- }
+ final Change updatedChange = db.changes().atomicUpdate(changeId,
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == Change.Status.DRAFT) {
+ change.setStatus(Change.Status.NEW);
+ ChangeUtil.updated(change);
}
- });
+ return change;
+ }
+ });
+
+ if (!updatedPatch.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+ hooks.doDraftPublishedHook(updatedChange, updatedPatch, db);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
index 7232755ece..53da2b6fdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
@@ -17,12 +17,15 @@ package com.google.gerrit.server.changedetail;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.ReviewResult;
+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.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RestoredSender;
import com.google.gerrit.server.project.ChangeControl;
@@ -31,93 +34,124 @@ import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
import java.util.concurrent.Callable;
-public class RestoreChange implements Callable<ReviewResult> {
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
- public interface Factory {
- RestoreChange create(PatchSet.Id patchSetId, String changeComment);
- }
+public class RestoreChange implements Callable<ReviewResult> {
private final RestoredSender.Factory restoredSenderFactory;
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
+ private final GitRepositoryManager repoManager;
private final IdentifiedUser currentUser;
private final ChangeHooks hooks;
- private final PatchSet.Id patchSetId;
- private final String changeComment;
+ @Argument(index = 0, required = true, multiValued = false,
+ usage = "change to restore", metaVar = "CHANGE")
+ private Change.Id changeId;
+ public void setChangeId(final Change.Id changeId) {
+ this.changeId = changeId;
+ }
+
+ @Option(name = "--message", aliases = {"-m"},
+ usage = "optional message to append to change")
+ private String message;
+ public void setMessage(final String message) {
+ this.message = message;
+ }
@Inject
RestoreChange(final RestoredSender.Factory restoredSenderFactory,
final ChangeControl.Factory changeControlFactory, final ReviewDb db,
- final IdentifiedUser currentUser, final ChangeHooks hooks,
- @Assisted final PatchSet.Id patchSetId,
- @Assisted final String changeComment) {
+ final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
+ final ChangeHooks hooks) {
this.restoredSenderFactory = restoredSenderFactory;
this.changeControlFactory = changeControlFactory;
this.db = db;
+ this.repoManager = repoManager;
this.currentUser = currentUser;
this.hooks = hooks;
- this.patchSetId = patchSetId;
- this.changeComment = changeComment;
+ changeId = null;
+ message = null;
}
@Override
- public ReviewResult call() throws EmailException,
- InvalidChangeOperationException, NoSuchChangeException, OrmException {
- final ReviewResult result = new ReviewResult();
+ public ReviewResult call() throws EmailException, NoSuchChangeException,
+ InvalidChangeOperationException, OrmException,
+ RepositoryNotFoundException, IOException {
+ if (changeId == null) {
+ throw new InvalidChangeOperationException("changeId is required");
+ }
- final Change.Id changeId = patchSetId.getParentKey();
+ final ReviewResult result = new ReviewResult();
result.setChangeId(changeId);
+
final ChangeControl control = changeControlFactory.validateFor(changeId);
- final PatchSet patch = db.patchSets().get(patchSetId);
+ final Change change = db.changes().get(changeId);
+ final PatchSet.Id patchSetId = change.currentPatchSetId();
if (!control.canRestore()) {
result.addError(new ReviewResult.Error(
ReviewResult.Error.Type.RESTORE_NOT_PERMITTED));
- } else if (patch == null) {
+ return result;
+ }
+
+ final PatchSet patch = db.patchSets().get(patchSetId);
+ if (patch == null) {
throw new NoSuchChangeException(changeId);
- } else {
-
- // Create a message to accompany the restored change
- final ChangeMessage cmsg =
- new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
- .messageUUID(db)), currentUser.getAccountId(), patchSetId);
- final StringBuilder msgBuf =
- new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
- if (changeComment != null && changeComment.length() > 0) {
- msgBuf.append("\n\n");
- msgBuf.append(changeComment);
- }
- cmsg.setMessage(msgBuf.toString());
-
- // Restore the change
- final Change updatedChange = db.changes().atomicUpdate(changeId,
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus() == Change.Status.ABANDONED
- && change.currentPatchSetId().equals(patchSetId)) {
- change.setStatus(Change.Status.NEW);
- ChangeUtil.updated(change);
- return change;
- } else {
- return null;
- }
- }
- });
+ }
+
+ final Branch.NameKey destBranch = control.getChange().getDest();
+ if (!ProjectUtil.branchExists(repoManager, destBranch)) {
+ result.addError(new ReviewResult.Error(
+ ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND, destBranch.get()));
+ return result;
+ }
- ChangeUtil.updatedChange(
- db, currentUser, updatedChange, cmsg, restoredSenderFactory,
- "Change is not abandoned or patchset is not latest");
+ // Create a message to accompany the restored change
+ final ChangeMessage cmsg =
+ new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
+ .messageUUID(db)), currentUser.getAccountId(), patchSetId);
+ final StringBuilder msgBuf =
+ new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
+ if (message != null && message.length() > 0) {
+ msgBuf.append("\n\n");
+ msgBuf.append(message);
+ }
+ cmsg.setMessage(msgBuf.toString());
+
+ // Restore the change
+ final Change updatedChange = db.changes().atomicUpdate(changeId,
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus() == Change.Status.ABANDONED) {
+ change.setStatus(Change.Status.NEW);
+ ChangeUtil.updated(change);
+ return change;
+ } else {
+ return null;
+ }
+ }
+ });
- hooks.doChangeRestoreHook(updatedChange, currentUser.getAccount(),
- changeComment, db);
+ if (updatedChange == null) {
+ result.addError(new ReviewResult.Error(
+ ReviewResult.Error.Type.CHANGE_NOT_ABANDONED));
+ return result;
}
+ ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+ restoredSenderFactory);
+ hooks.doChangeRestoredHook(updatedChange, currentUser.getAccount(),
+ message, db);
+
return result;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
index abd358292f..3287aa1d77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
@@ -24,6 +24,8 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.project.ChangeControl;
@@ -34,6 +36,7 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -49,6 +52,7 @@ public class Submit implements Callable<ReviewResult> {
private final MergeOp.Factory opFactory;
private final MergeQueue merger;
private final ReviewDb db;
+ private final GitRepositoryManager repoManager;
private final IdentifiedUser currentUser;
private final PatchSet.Id patchSetId;
@@ -56,12 +60,13 @@ public class Submit implements Callable<ReviewResult> {
@Inject
Submit(final ChangeControl.Factory changeControlFactory,
final MergeOp.Factory opFactory, final MergeQueue merger,
- final ReviewDb db, final IdentifiedUser currentUser,
- @Assisted final PatchSet.Id patchSetId) {
+ final ReviewDb db, final GitRepositoryManager repoManager,
+ final IdentifiedUser currentUser, @Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.opFactory = opFactory;
this.merger = merger;
this.db = db;
+ this.repoManager = repoManager;
this.currentUser = currentUser;
this.patchSetId = patchSetId;
@@ -69,7 +74,8 @@ public class Submit implements Callable<ReviewResult> {
@Override
public ReviewResult call() throws IllegalStateException,
- InvalidChangeOperationException, NoSuchChangeException, OrmException {
+ InvalidChangeOperationException, NoSuchChangeException, OrmException,
+ IOException {
final ReviewResult result = new ReviewResult();
final PatchSet patch = db.patchSets().get(patchSetId);
@@ -80,7 +86,7 @@ public class Submit implements Callable<ReviewResult> {
throw new NoSuchChangeException(changeId);
}
- List<SubmitRecord> submitResult = control.canSubmit(db, patchSetId);
+ List<SubmitRecord> submitResult = control.canSubmit(db, patch);
if (submitResult.isEmpty()) {
throw new IllegalStateException(
"ChangeControl.canSubmit returned empty list");
@@ -113,6 +119,10 @@ public class Submit implements Callable<ReviewResult> {
errMsg.append("change " + changeId + ": needs " + lbl.label);
break;
+ case MAY:
+ // The MAY label didn't cause the NOT_READY status
+ break;
+
case IMPOSSIBLE:
if (errMsg.length() > 0) errMsg.append("; ");
errMsg.append("change " + changeId + ": needs " + lbl.label
@@ -147,6 +157,14 @@ public class Submit implements Callable<ReviewResult> {
}
}
+ if (!ProjectUtil.branchExists(repoManager, control.getChange().getDest())) {
+ result.addError(new ReviewResult.Error(
+ ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND,
+ "Destination branch \"" + control.getChange().getDest().get()
+ + "\" not found."));
+ return result;
+ }
+
// Submit the change if we can
if (result.getErrors().isEmpty()) {
final List<PatchSetApproval> allApprovals =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index a0f0d365c1..9916257ad7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -36,12 +36,16 @@ public class AuthConfig {
private final AuthType authType;
private final String httpHeader;
private final boolean trustContainerAuth;
+ private final boolean userNameToLowerCase;
+ private final boolean gitBasicAuth;
private final String logoutUrl;
+ private final String openIdSsoUrl;
private final List<OpenIdProviderPattern> trustedOpenIDs;
private final List<OpenIdProviderPattern> allowedOpenIDs;
private final String cookiePath;
private final boolean cookieSecure;
private final SignedToken emailReg;
+ private final SignedToken restToken;
private final boolean allowGoogleAccountUpgrade;
@@ -51,11 +55,15 @@ public class AuthConfig {
authType = toType(cfg);
httpHeader = cfg.getString("auth", null, "httpheader");
logoutUrl = cfg.getString("auth", null, "logouturl");
+ openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
cookiePath = cfg.getString("auth", null, "cookiepath");
cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
+ gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
+ userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
+
String key = cfg.getString("auth", null, "registerEmailPrivateKey");
if (key != null && !key.isEmpty()) {
@@ -68,6 +76,15 @@ public class AuthConfig {
emailReg = null;
}
+ key = cfg.getString("auth", null, "restTokenPrivateKey");
+ if (key != null && !key.isEmpty()) {
+ int age = (int) ConfigUtil.getTimeUnit(cfg,
+ "auth", null, "maxRestTokenAge", 60, TimeUnit.SECONDS);
+ restToken = new SignedToken(age, key);
+ } else {
+ restToken = null;
+ }
+
if (authType == AuthType.OPENID) {
allowGoogleAccountUpgrade =
cfg.getBoolean("auth", "allowgoogleaccountupgrade", false);
@@ -106,6 +123,10 @@ public class AuthConfig {
return logoutUrl;
}
+ public String getOpenIdSsoUrl() {
+ return openIdSsoUrl;
+ }
+
public String getCookiePath() {
return cookiePath;
}
@@ -118,6 +139,10 @@ public class AuthConfig {
return emailReg;
}
+ public SignedToken getRestToken() {
+ return restToken;
+ }
+
public boolean isAllowGoogleAccountUpgrade() {
return allowGoogleAccountUpgrade;
}
@@ -132,6 +157,16 @@ public class AuthConfig {
return trustContainerAuth;
}
+ /** Whether user name should be converted to lower-case before validation */
+ public boolean isUserNameToLowerCase() {
+ return userNameToLowerCase;
+ }
+
+ /** Whether git-over-http should use Gerrit basic authentication scheme. */
+ public boolean isGitBasichAuth() {
+ return gitBasicAuth;
+ }
+
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
switch (getAuthType()) {
case DEVELOPMENT_BECOME_ANY_ACCOUNT:
@@ -146,6 +181,10 @@ public class AuthConfig {
//
return true;
+ case OPENID_SSO:
+ // There's only one provider in SSO mode, so it must be okay.
+ return true;
+
case OPENID:
// All identities must be trusted in order to trust the account.
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e76249a361..cc5405487b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -16,27 +16,10 @@ package com.google.gerrit.server.config;
import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
-
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-
import java.lang.reflect.InvocationTargetException;
-import java.text.MessageFormat;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -245,7 +228,7 @@ public class ConfigUtil {
*/
public static long getTimeUnit(final String valueString, long defaultValue,
TimeUnit wantUnit) {
- Matcher m = Pattern.compile("^([1-9][0-9]*)\\s*(.*)$").matcher(valueString);
+ Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
if (!m.matches()) {
return defaultValue;
}
@@ -303,83 +286,6 @@ public class ConfigUtil {
}
}
- /**
- * Resolve groups from group names, via the database. Group names not found in
- * the database will be skipped.
- *
- * @param dbfactory database to resolve from.
- * @param groupNames group names to resolve.
- * @param log log for any warnings and errors.
- * @param groupNotFoundWarning formatted message to output to the log for each
- * group name which is not found in the database. <code>{0}</code> will
- * be replaced with the group name.
- * @return the actual groups resolved from the database. If no groups are
- * found, returns an empty {@code Set}, never {@code null}.
- */
- public static Set<AccountGroup.UUID> groupsFor(
- SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
- String groupNotFoundWarning) {
- final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
- try {
- final ReviewDb db = dbfactory.open();
- try {
- List<AccountGroupName> groups = db.accountGroupNames().get(
- Iterables.transform(Arrays.asList(groupNames),
- new Function<String, AccountGroup.NameKey>() {
- @Override
- public AccountGroup.NameKey apply(String name) {
- return new AccountGroup.NameKey(name);
- }
- })).toList();
-
- Iterator<AccountGroup> ags = db.accountGroups().get(
- Iterables.transform(Iterables.filter(groups, Predicates.notNull()),
- new Function<AccountGroupName, AccountGroup.Id>() {
- @Override
- public AccountGroup.Id apply(AccountGroupName group) {
- return group.getId();
- }
- })).iterator();
-
- for (int i = 0; i < groupNames.length; i++) {
- if (groups.get(i) == null) {
- log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
- continue;
- }
- AccountGroup ag = ags.next();
- if (ag == null) {
- log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
- } else {
- result.add(ag.getGroupUUID());
- }
- }
- } finally {
- db.close();
- }
- } catch (OrmRuntimeException e) {
- log.error("Database error, cannot load groups", e);
- } catch (OrmException e) {
- log.error("Database error, cannot load groups", e);
- }
- return result;
- }
-
- /**
- * Resolve groups from group names, via the database. Group names not found in
- * the database will be skipped.
- *
- * @param dbfactory database to resolve from.
- * @param groupNames group names to resolve.
- * @param log log for any warnings and errors.
- * @return the actual groups resolved from the database. If no groups are
- * found, returns an empty {@code Set}, never {@code null}.
- */
- public static Set<AccountGroup.UUID> groupsFor(
- SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
- return groupsFor(dbfactory, groupNames, log,
- "Group \"{0}\" not in database, skipping.");
- }
-
private static boolean match(final String a, final String... cases) {
for (final String b : cases) {
if (equalsIgnoreCase(a, b)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index ecfe4f56b2..f259871485 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -14,8 +14,9 @@
package com.google.gerrit.server.config;
-import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.SystemConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -28,22 +29,33 @@ import java.util.Set;
/** Download protocol from {@code gerrit.config}. */
@Singleton
-public class DownloadSchemeConfig {
+public class DownloadConfig {
private final Set<DownloadScheme> downloadSchemes;
+ private final Set<DownloadCommand> downloadCommands;
@Inject
- DownloadSchemeConfig(@GerritServerConfig final Config cfg,
+ DownloadConfig(@GerritServerConfig final Config cfg,
final SystemConfig s) {
- List<DownloadScheme> all =
+ List<DownloadScheme> allSchemes =
ConfigUtil.getEnumList(cfg, "download", null, "scheme",
DownloadScheme.DEFAULT_DOWNLOADS);
-
downloadSchemes =
- Collections.unmodifiableSet(new HashSet<DownloadScheme>(all));
+ Collections.unmodifiableSet(new HashSet<DownloadScheme>(allSchemes));
+
+ List<DownloadCommand> allCommands =
+ ConfigUtil.getEnumList(cfg, "download", null, "command",
+ DownloadCommand.DEFAULT_DOWNLOADS);
+ downloadCommands =
+ Collections.unmodifiableSet(new HashSet<DownloadCommand>(allCommands));
}
/** Scheme used to download. */
- public Set<DownloadScheme> getDownloadScheme() {
+ public Set<DownloadScheme> getDownloadSchemes() {
return downloadSchemes;
}
+
+ /** Command used to download. */
+ public Set<DownloadCommand> getDownloadCommands() {
+ return downloadCommands;
+ }
}
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 99dd54a6a9..2a66706c17 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
@@ -16,14 +16,21 @@ package com.google.gerrit.server.config;
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.cache.Cache;
+import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
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.FileTypeRegistry;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.ReplicationUser;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -32,19 +39,23 @@ import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.DefaultRealm;
import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCacheImpl;
+import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.GroupInfoCacheFactory;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.IncludingGroupMembership;
+import com.google.gerrit.server.account.InternalGroupBackend;
import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.UniversalGroupBackend;
import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.ChangeMergeQueue;
import com.google.gerrit.server.git.GitModule;
import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.git.PushAllProjectsOp;
import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.gerrit.server.git.SecureCredentialsProvider;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -62,8 +73,10 @@ import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
import org.apache.velocity.runtime.RuntimeInstance;
import org.eclipse.jgit.lib.Config;
@@ -115,26 +128,31 @@ public class GerritGlobalModule extends FactoryModule {
install(new AccessControlModule());
install(new GitModule());
install(new PrologModule());
+ install(ThreadLocalRequestContext.module());
factory(AccountInfoCacheFactory.Factory.class);
factory(CapabilityControl.Factory.class);
factory(GroupInfoCacheFactory.Factory.class);
+ factory(InternalUser.Factory.class);
factory(ProjectNode.Factory.class);
factory(ProjectState.Factory.class);
- factory(MaterializedGroupMembership.Factory.class);
bind(PermissionCollection.Factory.class);
bind(AccountVisibility.class)
.toProvider(AccountVisibilityProvider.class)
.in(SINGLETON);
+ bind(GroupControl.Factory.class).in(SINGLETON);
+ factory(IncludingGroupMembership.Factory.class);
+ bind(InternalGroupBackend.class).in(SINGLETON);
+ bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
+ DynamicSet.setOf(binder(), GroupBackend.class);
+ DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
+
bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
bind(ToolsCatalog.class);
bind(EventFactory.class);
bind(TransferConfig.class);
- factory(SecureCredentialsProvider.Factory.class);
- factory(PushAllProjectsOp.Factory.class);
-
bind(ChangeMergeQueue.class).in(SINGLETON);
bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
factory(ReloadSubmitQueueOp.Factory.class);
@@ -150,6 +168,15 @@ public class GerritGlobalModule extends FactoryModule {
bind(ChangeControl.GenericFactory.class);
bind(ProjectControl.GenericFactory.class);
factory(FunctionState.Factory.class);
- factory(ReplicationUser.Factory.class);
+
+ install(new AuditModule());
+
+ bind(GitReferenceUpdated.class);
+ DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
+ DynamicSet.setOf(binder(), CacheRemovalListener.class);
+ DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+ DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+
+ bind(AnonymousUser.class);
}
}
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 71bcc6cce7..ba54c56c84 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
@@ -17,26 +17,25 @@ package com.google.gerrit.server.config;
import static com.google.inject.Scopes.SINGLETON;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupDetailFactory;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.PerformCreateGroup;
import com.google.gerrit.server.account.PerformRenameGroup;
import com.google.gerrit.server.account.VisibleGroups;
-import com.google.gerrit.server.changedetail.AbandonChange;
import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
import com.google.gerrit.server.changedetail.PublishDraft;
-import com.google.gerrit.server.changedetail.RestoreChange;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NotesBranchUtil;
import com.google.gerrit.server.git.SubmoduleOp;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.AddReviewerSender;
@@ -73,11 +72,10 @@ public class GerritRequestModule extends FactoryModule {
bind(AccountResolver.class);
bind(ChangeQueryRewriter.class);
bind(ListProjects.class);
+ bind(ApprovalsUtil.class);
- bind(AnonymousUser.class).in(RequestScoped.class);
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
bind(ChangeControl.Factory.class).in(SINGLETON);
- bind(GroupControl.Factory.class).in(SINGLETON);
bind(ProjectControl.Factory.class).in(SINGLETON);
bind(AccountControl.Factory.class).in(SINGLETON);
@@ -85,12 +83,12 @@ public class GerritRequestModule extends FactoryModule {
factory(SubmoduleOp.Factory.class);
factory(MergeOp.Factory.class);
factory(CreateCodeReviewNotes.Factory.class);
+ factory(NotesBranchUtil.Factory.class);
install(new AsyncReceiveCommits.Module());
// Not really per-request, but dammit, I don't know where else to
// easily park this stuff.
//
- factory(AbandonChange.Factory.class);
factory(AddReviewer.Factory.class);
factory(AddReviewerSender.Factory.class);
factory(CreateChangeSender.Factory.class);
@@ -101,7 +99,6 @@ public class GerritRequestModule extends FactoryModule {
factory(RebasedPatchSetSender.Factory.class);
factory(AbandonedSender.Factory.class);
factory(RemoveReviewer.Factory.class);
- factory(RestoreChange.Factory.class);
factory(RestoredSender.Factory.class);
factory(RevertedSender.Factory.class);
factory(CommentSender.Factory.class);
@@ -115,5 +112,6 @@ public class GerritRequestModule extends FactoryModule {
factory(CreateProject.Factory.class);
factory(Submit.Factory.class);
factory(SuggestParentCandidates.Factory.class);
+ factory(BanCommit.Factory.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 9992f18752..8b517a3ffc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -15,8 +15,7 @@
package com.google.gerrit.server.config;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -25,9 +24,9 @@ import java.util.Collections;
public class GitReceivePackGroupsProvider extends GroupSetProvider {
@Inject
- public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db) {
- super(config, db, "receive", null, "allowGroup");
+ public GitReceivePackGroupsProvider(GroupBackend gb,
+ @GerritServerConfig Config config) {
+ super(gb, config, "receive", null, "allowGroup");
// If no group was set, default to "registered users"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 76d8844773..c519902293 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -15,8 +15,7 @@
package com.google.gerrit.server.config;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -26,9 +25,9 @@ import java.util.HashSet;
public class GitUploadPackGroupsProvider extends GroupSetProvider {
@Inject
- public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db) {
- super(config, db, "upload", null, "allowGroup");
+ public GitUploadPackGroupsProvider(GroupBackend gb,
+ @GerritServerConfig Config config) {
+ super(gb, config, "upload", null, "allowGroup");
// If no group was set, default to "registered users" and "anonymous"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 15711afef4..5fa243b6d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -14,12 +14,11 @@
package com.google.gerrit.server.config;
-import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
-import static java.util.Collections.unmodifiableSet;
-
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -37,10 +36,20 @@ public abstract class GroupSetProvider implements
protected Set<AccountGroup.UUID> groupIds;
@Inject
- protected GroupSetProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db, String section, String subsection, String name) {
+ protected GroupSetProvider(GroupBackend groupBackend,
+ @GerritServerConfig Config config, String section,
+ String subsection, String name) {
String[] groupNames = config.getStringList(section, subsection, name);
- groupIds = unmodifiableSet(groupsFor(db, groupNames, log));
+ ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+ for (String n : groupNames) {
+ GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
+ if (g == null) {
+ log.warn("Group \"{0}\" not in database, skipping.", n);
+ } else {
+ builder.add(g.getUUID());
+ }
+ }
+ groupIds = builder.build();
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index 26c76c5878..bb95dca706 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -14,14 +14,11 @@
package com.google.gerrit.server.config;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.git.PushAllProjectsOp;
import com.google.gerrit.server.git.ReloadSubmitQueueOp;
import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
import java.util.concurrent.TimeUnit;
/** Configuration for a master node in a cluster of servers. */
@@ -32,26 +29,15 @@ public class MasterNodeStartup extends LifecycleModule {
}
static class OnStart implements LifecycleListener {
- private final PushAllProjectsOp.Factory pushAll;
private final ReloadSubmitQueueOp.Factory submit;
- private final boolean replicateOnStartup;
@Inject
- OnStart(final PushAllProjectsOp.Factory pushAll,
- final ReloadSubmitQueueOp.Factory submit,
- final @GerritServerConfig Config cfg) {
- this.pushAll = pushAll;
+ OnStart(final ReloadSubmitQueueOp.Factory submit) {
this.submit = submit;
-
- replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true);
}
@Override
public void start() {
- if (replicateOnStartup) {
- pushAll.create(null).start(30, TimeUnit.SECONDS);
- }
-
submit.create().start(15, TimeUnit.SECONDS);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index b279086ca5..6622b0f160 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,8 +14,7 @@
package com.google.gerrit.server.config;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -33,8 +32,8 @@ import org.eclipse.jgit.lib.Config;
*/
public class ProjectOwnerGroupsProvider extends GroupSetProvider {
@Inject
- public ProjectOwnerGroupsProvider(
- @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
- super(config, db, "repository", "*", "ownerGroup");
+ public ProjectOwnerGroupsProvider(GroupBackend gb,
+ @GerritServerConfig final Config config) {
+ super(gb, config, "repository", "*", "ownerGroup");
}
}
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 ab52a9de99..8d76e90397 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
@@ -28,7 +28,10 @@ public final class SitePaths {
public final File bin_dir;
public final File etc_dir;
public final File lib_dir;
+ public final File tmp_dir;
public final File logs_dir;
+ public final File plugins_dir;
+ public final File data_dir;
public final File mail_dir;
public final File hooks_dir;
public final File static_dir;
@@ -38,7 +41,6 @@ public final class SitePaths {
public final File gerrit_config;
public final File secure_config;
- public final File replication_config;
public final File contact_information_pub;
public final File ssl_keystore;
@@ -62,6 +64,9 @@ public final class SitePaths {
bin_dir = new File(site_path, "bin");
etc_dir = new File(site_path, "etc");
lib_dir = new File(site_path, "lib");
+ tmp_dir = new File(site_path, "tmp");
+ plugins_dir = new File(site_path, "plugins");
+ data_dir = new File(site_path, "data");
logs_dir = new File(site_path, "logs");
mail_dir = new File(etc_dir, "mail");
hooks_dir = new File(site_path, "hooks");
@@ -72,7 +77,6 @@ public final class SitePaths {
gerrit_config = new File(etc_dir, "gerrit.config");
secure_config = new File(etc_dir, "secure.config");
- replication_config = new File(etc_dir, "replication.config");
contact_information_pub = new File(etc_dir, "contact_information.pub");
ssl_keystore = new File(etc_dir, "keystore");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
new file mode 100644
index 0000000000..6bb7ba8e7e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,145 @@
+// 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.documentation;
+
+import static org.pegdown.Extensions.ALL;
+import static org.pegdown.Extensions.HARDWRAPS;
+
+import com.google.common.base.Strings;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.pegdown.LinkRenderer;
+import org.pegdown.PegDownProcessor;
+import org.pegdown.ToHtmlSerializer;
+import org.pegdown.ast.HeaderNode;
+import org.pegdown.ast.Node;
+import org.pegdown.ast.RootNode;
+import org.pegdown.ast.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class MarkdownFormatter {
+ private static final Logger log =
+ LoggerFactory.getLogger(MarkdownFormatter.class);
+
+ private static final String css;
+
+ static {
+ AtomicBoolean file = new AtomicBoolean();
+ String src;
+ try {
+ src = readPegdownCss(file);
+ } catch (IOException err) {
+ log.warn("Cannot load pegdown.css", err);
+ src = "";
+ }
+ css = file.get() ? null : src;
+ }
+
+ private static String readCSS() {
+ if (css != null) {
+ return css;
+ }
+ try {
+ return readPegdownCss(new AtomicBoolean());
+ } catch (IOException err) {
+ log.warn("Cannot load pegdown.css", err);
+ return "";
+ }
+ }
+
+ public byte[] markdownToDocHtml(String md, String charEnc)
+ throws UnsupportedEncodingException {
+ RootNode root = parseMarkdown(md);
+ String title = findTitle(root);
+
+ StringBuilder html = new StringBuilder();
+ html.append("<html>");
+ html.append("<head>");
+ if (!Strings.isNullOrEmpty(title)) {
+ html.append("<title>").append(title).append("</title>");
+ }
+ html.append("<style type=\"text/css\">\n")
+ .append(readCSS())
+ .append("\n</style>");
+ html.append("</head>");
+ html.append("<body>\n");
+ html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
+ html.append("\n</body></html>");
+ return html.toString().getBytes(charEnc);
+ }
+
+ public String extractTitleFromMarkdown(byte[] data, String charEnc) {
+ String md = RawParseUtils.decode(Charset.forName(charEnc), data);
+ return findTitle(parseMarkdown(md));
+ }
+
+ private String findTitle(Node root) {
+ if (root instanceof HeaderNode) {
+ HeaderNode h = (HeaderNode) root;
+ if (h.getLevel() == 1
+ && h.getChildren() != null
+ && !h.getChildren().isEmpty()) {
+ StringBuilder b = new StringBuilder();
+ for (Node n : root.getChildren()) {
+ if (n instanceof TextNode) {
+ b.append(((TextNode) n).getText());
+ }
+ }
+ return b.toString();
+ }
+ }
+
+ for (Node n : root.getChildren()) {
+ String title = findTitle(n);
+ if (title != null) {
+ return title;
+ }
+ }
+ return null;
+ }
+
+ private RootNode parseMarkdown(String md) {
+ return new PegDownProcessor(ALL & ~(HARDWRAPS))
+ .parseMarkdown(md.toCharArray());
+ }
+
+ private static String readPegdownCss(AtomicBoolean file)
+ throws IOException {
+ String name = "pegdown.css";
+ URL url = MarkdownFormatter.class.getResource(name);
+ if (url == null) {
+ throw new FileNotFoundException("Resource " + name);
+ }
+ file.set("file".equals(url.getProtocol()));
+ InputStream in = url.openStream();
+ try {
+ TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
+ tmp.copy(in);
+ return new String(tmp.toByteArray(), "UTF-8");
+ } finally {
+ in.close();
+ }
+ }
+}
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/events/AccountAttribute.java
index 2ad7ffe4f5..2d88b834d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
@@ -17,4 +17,5 @@ package com.google.gerrit.server.events;
public class AccountAttribute {
public String name;
public String email;
+ public String username;
}
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/events/ChangeAttribute.java
index 9810f59d28..5150b48e2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
@@ -42,4 +42,5 @@ public class ChangeAttribute {
public List<DependencyAttribute> dependsOn;
public List<DependencyAttribute> neededBy;
+ public List<SubmitRecordAttribute> submitRecords;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 1a2922bb33..717e23cc05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.events;
-public class ChangeRestoreEvent extends ChangeEvent {
+public class ChangeRestoredEvent extends ChangeEvent {
public final String type = "change-restored";
public ChangeAttribute change;
public PatchSetAttribute patchSet;
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
new file mode 100644
index 0000000000..c90ac90de1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
@@ -0,0 +1,22 @@
+// 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.events;
+
+public class DraftPublishedEvent extends ChangeEvent {
+ public final String type = "draft-published";
+ public ChangeAttribute change;
+ public PatchSetAttribute patchSet;
+ public AccountAttribute uploader;
+}
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 4d34b716f1..f07ae0c53d 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
@@ -16,6 +16,7 @@ package com.google.gerrit.server.events;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@@ -27,11 +28,13 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
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.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -39,31 +42,39 @@ import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
+import java.util.List;
import javax.annotation.Nullable;
@Singleton
public class EventFactory {
+ private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
private final AccountCache accountCache;
private final Provider<String> urlProvider;
private final ApprovalTypes approvalTypes;
private final PatchListCache patchListCache;
private final SchemaFactory<ReviewDb> schema;
+ private final PersonIdent myIdent;
@Inject
EventFactory(AccountCache accountCache,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
ApprovalTypes approvalTypes,
- PatchListCache patchListCache, SchemaFactory<ReviewDb> schema) {
+ PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
+ @GerritPersonIdent PersonIdent myIdent) {
this.accountCache = accountCache;
this.urlProvider = urlProvider;
this.approvalTypes = approvalTypes;
this.patchListCache = patchListCache;
this.schema = schema;
+ this.myIdent = myIdent;
}
/**
@@ -117,6 +128,47 @@ public class EventFactory {
a.status = change.getStatus();
}
+ /**
+ * Add submitRecords to an existing ChangeAttribute.
+ *
+ * @param ca
+ * @param submitRecords
+ */
+ public void addSubmitRecords(ChangeAttribute ca,
+ List<SubmitRecord> submitRecords) {
+ ca.submitRecords = new ArrayList<SubmitRecordAttribute>();
+
+ for (SubmitRecord submitRecord : submitRecords) {
+ SubmitRecordAttribute sa = new SubmitRecordAttribute();
+ sa.status = submitRecord.status.name();
+ if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
+ addSubmitRecordLabels(submitRecord, sa);
+ }
+ ca.submitRecords.add(sa);
+ }
+ // Remove empty lists so a confusing label won't be displayed in the output.
+ if (ca.submitRecords.isEmpty()) {
+ ca.submitRecords = null;
+ }
+ }
+
+ private void addSubmitRecordLabels(SubmitRecord submitRecord,
+ SubmitRecordAttribute sa) {
+ if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) {
+ sa.labels = new ArrayList<SubmitLabelAttribute>();
+ for (SubmitRecord.Label lbl : submitRecord.labels) {
+ SubmitLabelAttribute la = new SubmitLabelAttribute();
+ la.label = lbl.label;
+ la.status = lbl.status.name();
+ if(lbl.appliedBy != null) {
+ Account a = accountCache.get(lbl.appliedBy).getAccount();
+ la.by = asAccountAttribute(a);
+ }
+ sa.labels.add(la);
+ }
+ }
+ }
+
public void addDependencies(ChangeAttribute ca, Change change) {
ca.dependsOn = new ArrayList<DependencyAttribute>();
ca.neededBy = new ArrayList<DependencyAttribute>();
@@ -229,16 +281,19 @@ public class EventFactory {
public void addPatchSetFileNames(PatchSetAttribute patchSetAttribute,
Change change, PatchSet patchSet) {
- PatchList patchList = patchListCache.get(change, patchSet);
- for (PatchListEntry patch : patchList.getPatches()) {
- if (patchSetAttribute.files == null) {
- patchSetAttribute.files = new ArrayList<PatchAttribute>();
- }
+ try {
+ PatchList patchList = patchListCache.get(change, patchSet);
+ for (PatchListEntry patch : patchList.getPatches()) {
+ if (patchSetAttribute.files == null) {
+ patchSetAttribute.files = new ArrayList<PatchAttribute>();
+ }
- PatchAttribute p = new PatchAttribute();
- p.file = patch.getNewName();
- p.type = patch.getChangeType();
- patchSetAttribute.files.add(p);
+ PatchAttribute p = new PatchAttribute();
+ p.file = patch.getNewName();
+ p.type = patch.getChangeType();
+ patchSetAttribute.files.add(p);
+ }
+ } catch (PatchListNotAvailableException e) {
}
}
@@ -273,6 +328,20 @@ public class EventFactory {
p.ref = patchSet.getRefName();
p.uploader = asAccountAttribute(patchSet.getUploader());
p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+ try {
+ final ReviewDb db = schema.open();
+ try {
+ p.parents = new ArrayList<String>();
+ for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
+ patchSet.getId())) {
+ p.parents.add(a.getAncestorRevision().get());
+ }
+ } finally {
+ db.close();
+ }
+ } catch (OrmException e) {
+ log.error("Cannot load patch set data for " + patchSet.getId(), e);
+ }
return p;
}
@@ -321,6 +390,21 @@ public class EventFactory {
AccountAttribute who = new AccountAttribute();
who.name = account.getFullName();
who.email = account.getPreferredEmail();
+ who.username = account.getUserName();
+ return who;
+ }
+
+ /**
+ * Create an AuthorAttribute for the given person ident suitable for
+ * serialization to JSON.
+ *
+ * @param ident
+ * @return object suitable for serialization to JSON
+ */
+ public AccountAttribute asAccountAttribute(PersonIdent ident) {
+ AccountAttribute who = new AccountAttribute();
+ who.name = ident.getName();
+ who.email = ident.getEmailAddress();
return who;
}
@@ -348,7 +432,9 @@ public class EventFactory {
public MessageAttribute asMessageAttribute(ChangeMessage message) {
MessageAttribute a = new MessageAttribute();
a.timestamp = message.getWrittenOn().getTime() / 1000L;
- a.reviewer = asAccountAttribute(message.getAuthor());
+ a.reviewer =
+ message.getAuthor() != null ? asAccountAttribute(message.getAuthor())
+ : asAccountAttribute(myIdent);
a.message = message.getMessage();
return a;
}
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/events/PatchSetAttribute.java
index dca44384ca..f726ce3085 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
@@ -17,13 +17,14 @@ package com.google.gerrit.server.events;
import java.util.List;
public class PatchSetAttribute {
- public String number;
- public String revision;
- public String ref;
- public AccountAttribute uploader;
- public Long createdOn;
+ public String number;
+ public String revision;
+ public List<String> parents;
+ public String ref;
+ public AccountAttribute uploader;
+ public Long createdOn;
- public List<ApprovalAttribute> approvals;
- public List<PatchSetCommentAttribute> comments;
- public List<PatchAttribute> files;
+ public List<ApprovalAttribute> approvals;
+ public List<PatchSetCommentAttribute> comments;
+ public List<PatchAttribute> files;
}
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/events/SubmitLabelAttribute.java
new file mode 100644
index 0000000000..99d0350842
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
@@ -0,0 +1,21 @@
+// 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.events;
+
+public class SubmitLabelAttribute {
+ public String label;
+ public String status;
+ public AccountAttribute by;
+}
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/events/SubmitRecordAttribute.java
new file mode 100644
index 0000000000..04b76e1287
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
@@ -0,0 +1,22 @@
+// 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.events;
+
+import java.util.List;
+
+public class SubmitRecordAttribute {
+ public String status;
+ public List<SubmitLabelAttribute> labels;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
new file mode 100644
index 0000000000..833b6112d4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -0,0 +1,73 @@
+// 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.extensions.events;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.List;
+
+public class GitReferenceUpdated {
+ public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
+ Collections.<GitReferenceUpdatedListener> emptyList());
+
+ private final Iterable<GitReferenceUpdatedListener> listeners;
+
+ @Inject
+ GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ GitReferenceUpdated(Iterable<GitReferenceUpdatedListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ public void fire(Project.NameKey project, String ref) {
+ Event event = new Event(project, ref);
+ for (GitReferenceUpdatedListener l : listeners) {
+ l.onGitReferenceUpdated(event);
+ }
+ }
+
+ private static class Event implements GitReferenceUpdatedListener.Event {
+ private final String projectName;
+ private final String ref;
+
+ Event(Project.NameKey project, String ref) {
+ this.projectName = project.get();
+ this.ref = ref;
+ }
+
+ @Override
+ public String getProjectName() {
+ return projectName;
+ }
+
+ @Override
+ public List<GitReferenceUpdatedListener.Update> getUpdates() {
+ GitReferenceUpdatedListener.Update update =
+ new GitReferenceUpdatedListener.Update() {
+ public String getRefName() {
+ return ref;
+ }
+ };
+ return ImmutableList.of(update);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
index c1b029292f..7d868bf854 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
@@ -12,29 +12,24 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.git;
-/** Proxy around a cache which has not yet been created. */
-public final class ProxyCache<K, V> implements Cache<K, V> {
- private volatile Cache<K, V> self;
+import com.google.gerrit.common.data.PermissionRule;
- public void bind(Cache<K, V> self) {
- this.self = self;
- }
-
- public V get(K key) {
- return self.get(key);
- }
+import java.util.ArrayList;
+import java.util.List;
- public void put(K key, V value) {
- self.put(key, value);
- }
+public class AccountsSection {
+ protected List<PermissionRule> sameGroupVisibility;
- public void remove(K key) {
- self.remove(key);
+ public List<PermissionRule> getSameGroupVisibility() {
+ if (sameGroupVisibility == null) {
+ sameGroupVisibility = new ArrayList<PermissionRule>();
+ }
+ return sameGroupVisibility;
}
- public void removeAll() {
- self.removeAll();
+ public void setSameGroupVisibility(List<PermissionRule> sameGroupVisibility) {
+ this.sameGroupVisibility = sameGroupVisibility;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
new file mode 100644
index 0000000000..c0e00aaa37
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -0,0 +1,156 @@
+// 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.git;
+
+import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+public class BanCommit {
+ public interface Factory {
+ BanCommit create();
+ }
+
+ private final Provider<IdentifiedUser> currentUser;
+ private final GitRepositoryManager repoManager;
+ private final PersonIdent gerritIdent;
+ private NotesBranchUtil.Factory notesBranchUtilFactory;
+
+ @Inject
+ BanCommit(final Provider<IdentifiedUser> currentUser,
+ final GitRepositoryManager repoManager,
+ @GerritPersonIdent final PersonIdent gerritIdent,
+ final NotesBranchUtil.Factory notesBranchUtilFactory) {
+ this.currentUser = currentUser;
+ this.repoManager = repoManager;
+ this.gerritIdent = gerritIdent;
+ this.notesBranchUtilFactory = notesBranchUtilFactory;
+ }
+
+ public BanCommitResult ban(final ProjectControl projectControl,
+ final List<ObjectId> commitsToBan, final String reason)
+ throws PermissionDeniedException, IOException,
+ InterruptedException, MergeException, ConcurrentRefUpdateException {
+ if (!projectControl.isOwner()) {
+ throw new PermissionDeniedException(
+ "No project owner: not permitted to ban commits");
+ }
+
+ final BanCommitResult result = new BanCommitResult();
+ NoteMap banCommitNotes = NoteMap.newEmptyMap();
+ // add a note for each banned commit to notes
+ final Repository repo =
+ repoManager.openRepository(projectControl.getProject().getNameKey());
+ try {
+ final RevWalk revWalk = new RevWalk(repo);
+ final ObjectInserter inserter = repo.newObjectInserter();
+ try {
+ for (final ObjectId commitToBan : commitsToBan) {
+ try {
+ revWalk.parseCommit(commitToBan);
+ } catch (MissingObjectException e) {
+ // ignore exception, also not existing commits can be banned
+ } catch (IncorrectObjectTypeException e) {
+ result.notACommit(commitToBan, e.getMessage());
+ continue;
+ }
+ banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
+ }
+ inserter.flush();
+ NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(repo);
+ NoteMap newlyCreated =
+ notesBranchUtil.commitNewNotes(banCommitNotes, REF_REJECT_COMMITS,
+ createPersonIdent(), buildCommitMessage(commitsToBan, reason));
+
+ for (Note n : banCommitNotes) {
+ if (newlyCreated.contains(n)) {
+ result.commitBanned(n);
+ } else {
+ result.commitAlreadyBanned(n);
+ }
+ }
+ return result;
+ } finally {
+ revWalk.release();
+ inserter.release();
+ }
+ } finally {
+ repo.close();
+ }
+ }
+
+ private ObjectId createNoteContent(String reason, ObjectInserter inserter)
+ throws UnsupportedEncodingException, IOException {
+ String noteContent = reason != null ? reason : "";
+ if (noteContent.length() > 0 && !noteContent.endsWith("\n")) {
+ noteContent = noteContent + "\n";
+ }
+ return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+ }
+
+ private PersonIdent createPersonIdent() {
+ Date now = new Date();
+ TimeZone tz = gerritIdent.getTimeZone();
+ return currentUser.get().newCommitterIdent(now, tz);
+ }
+
+ private static String buildCommitMessage(final List<ObjectId> bannedCommits,
+ final String reason) {
+ final StringBuilder commitMsg = new StringBuilder();
+ commitMsg.append("Banning ");
+ commitMsg.append(bannedCommits.size());
+ commitMsg.append(" ");
+ commitMsg.append(bannedCommits.size() == 1 ? "commit" : "commits");
+ commitMsg.append("\n\n");
+ if (reason != null) {
+ commitMsg.append("Reason: ");
+ commitMsg.append(reason);
+ commitMsg.append("\n\n");
+ }
+ commitMsg.append("The following commits are banned:\n");
+ final StringBuilder commitList = new StringBuilder();
+ for (final ObjectId c : bannedCommits) {
+ if (commitList.length() > 0) {
+ commitList.append(",\n");
+ }
+ commitList.append(c.getName());
+ }
+ commitMsg.append(commitList);
+ return commitMsg.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
new file mode 100644
index 0000000000..1b4845502f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -0,0 +1,54 @@
+// 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.git;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class BanCommitResult {
+
+ private final List<ObjectId> newlyBannedCommits = new LinkedList<ObjectId>();
+ private final List<ObjectId> alreadyBannedCommits = new LinkedList<ObjectId>();
+ private final List<ObjectId> ignoredObjectIds = new LinkedList<ObjectId>();
+
+ public BanCommitResult() {
+ }
+
+ public void commitBanned(final ObjectId commitId) {
+ newlyBannedCommits.add(commitId);
+ }
+
+ public void commitAlreadyBanned(final ObjectId commitId) {
+ alreadyBannedCommits.add(commitId);
+ }
+
+ public void notACommit(final ObjectId id, final String message) {
+ ignoredObjectIds.add(id);
+ }
+
+ public List<ObjectId> getNewlyBannedCommits() {
+ return newlyBannedCommits;
+ }
+
+ public List<ObjectId> getAlreadyBannedCommits() {
+ return alreadyBannedCommits;
+ }
+
+ public List<ObjectId> getIgnoredObjectIds() {
+ return ignoredObjectIds;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index 86e0740d18..6d1b1556a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -20,16 +20,17 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
+import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.servlet.RequestScoped;
@@ -43,6 +44,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Singleton
@@ -57,6 +59,7 @@ public class ChangeMergeQueue implements MergeQueue {
private final WorkQueue workQueue;
private final Provider<MergeOp.Factory> bgFactory;
+ private final PerThreadRequestScope.Scoper threadScoper;
@Inject
ChangeMergeQueue(final WorkQueue wq, Injector parent) {
@@ -68,15 +71,9 @@ public class ChangeMergeQueue implements MergeQueue {
bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
bind(RequestScopePropagator.class)
.to(PerThreadRequestScope.Propagator.class);
+ bind(PerThreadRequestScope.Propagator.class);
install(new GerritRequestModule());
- bind(CurrentUser.class).to(IdentifiedUser.class);
- bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
- @Override
- public IdentifiedUser get() {
- throw new OutOfScopeException("No user on merge thread");
- }
- });
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
new Provider<SocketAddress>() {
@Override
@@ -91,8 +88,26 @@ public class ChangeMergeQueue implements MergeQueue {
}
});
}
+
+ @Provides
+ public PerThreadRequestScope.Scoper provideScoper(
+ final PerThreadRequestScope.Propagator propagator) {
+ final RequestContext requestContext = new RequestContext() {
+ @Override
+ public CurrentUser getCurrentUser() {
+ throw new OutOfScopeException("No user on merge thread");
+ }
+ };
+ return new PerThreadRequestScope.Scoper() {
+ @Override
+ public <T> Callable<T> scope(Callable<T> callable) {
+ return propagator.scope(requestContext, callable);
+ }
+ };
+ }
});
bgFactory = child.getProvider(MergeOp.Factory.class);
+ threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
}
@Override
@@ -186,19 +201,15 @@ public class ChangeMergeQueue implements MergeQueue {
}
}
- private void mergeImpl(Branch.NameKey branch) {
+ private void mergeImpl(final Branch.NameKey branch) {
try {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- try {
+ threadScoper.scope(new Callable<Void>(){
+ @Override
+ public Void call() throws Exception {
bgFactory.get().create(branch).merge();
- } finally {
- ctx.cleanup.run();
+ return null;
}
- } finally {
- PerThreadRequestScope.set(old);
- }
+ }).call();
} catch (Throwable e) {
log.error("Merge attempt for " + branch + " failed", e);
} finally {
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 87903511fc..6b53121b1a 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
@@ -51,7 +51,26 @@ enum CommitMergeStatus {
/** */
NOT_FAST_FORWARD("Project policy requires all submissions to be a fast-forward.\n"
+ "\n"
- + "Please rebase the change locally and upload again for review.");
+ + "Please rebase the change locally and upload again for review."),
+
+ /** */
+ INVALID_PROJECT_CONFIGURATION("Change contains an invalid project configuration."),
+
+ /** */
+ INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND(
+ "Change contains an invalid project configuration:\n"
+ + "Parent project does not exist."),
+
+ /** */
+ INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT(
+ "Change contains an invalid project configuration:\n"
+ + "The root project cannot have a parent."),
+
+ /** */
+ SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN(
+ "Change contains a project configuration that changes the parent project.\n"
+ + "The change must be submitted by a Gerrit administrator.");
+
private String message;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
index 6fea8f1988..9a0fe17e0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
@@ -20,6 +20,7 @@ import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
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.gerrit.server.GerritPersonIdent;
@@ -31,23 +32,15 @@ import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.notes.NoteMapMerger;
-import org.eclipse.jgit.notes.NoteMerger;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -68,29 +61,21 @@ public class CreateCodeReviewNotes {
CreateCodeReviewNotes create(ReviewDb reviewDb, Repository db);
}
- private static final int MAX_LOCK_FAILURE_CALLS = 10;
- private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
- private final ReviewDb schema;
- private final PersonIdent gerritIdent;
private final AccountCache accountCache;
private final ApprovalTypes approvalTypes;
private final String canonicalWebUrl;
private final String anonymousCowardName;
+ private final ReviewDb schema;
private final Repository db;
- private final RevWalk revWalk;
- private final ObjectInserter inserter;
- private final ObjectReader reader;
- private RevCommit baseCommit;
- private NoteMap base;
+ private PersonIdent author;
- private RevCommit oursCommit;
- private NoteMap ours;
+ private RevWalk revWalk;
+ private ObjectInserter inserter;
- private List<CodeReviewCommit> commits;
- private PersonIdent author;
+ private final NotesBranchUtil.Factory notesBranchUtilFactory;
@Inject
CreateCodeReviewNotes(
@@ -99,90 +84,89 @@ public class CreateCodeReviewNotes {
final ApprovalTypes approvalTypes,
final @Nullable @CanonicalWebUrl String canonicalWebUrl,
final @AnonymousCowardName String anonymousCowardName,
+ final NotesBranchUtil.Factory notesBranchUtilFactory,
final @Assisted ReviewDb reviewDb,
final @Assisted Repository db) {
- schema = reviewDb;
this.author = gerritIdent;
- this.gerritIdent = gerritIdent;
this.accountCache = accountCache;
this.approvalTypes = approvalTypes;
this.canonicalWebUrl = canonicalWebUrl;
this.anonymousCowardName = anonymousCowardName;
+ this.notesBranchUtilFactory = notesBranchUtilFactory;
+ schema = reviewDb;
this.db = db;
-
- revWalk = new RevWalk(db);
- inserter = db.newObjectInserter();
- reader = db.newObjectReader();
}
public void create(List<CodeReviewCommit> commits, PersonIdent author)
throws CodeReviewNoteCreationException {
try {
- this.commits = commits;
- this.author = author;
- loadBase();
- applyNotes();
- updateRef();
+ revWalk = new RevWalk(db);
+ inserter = db.newObjectInserter();
+ if (author != null) {
+ this.author = author;
+ }
+
+ NoteMap notes = NoteMap.newEmptyMap();
+ StringBuilder message =
+ new StringBuilder("Update notes for submitted changes\n\n");
+ for (CodeReviewCommit c : commits) {
+ notes.set(c, createNoteContent(c.change, c));
+ message.append("* ").append(c.getShortMessage()).append("\n");
+ }
+
+ NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
+ notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
+ message.toString());
+ inserter.flush();
} catch (IOException e) {
throw new CodeReviewNoteCreationException(e);
- } catch (InterruptedException e) {
+ } catch (ConcurrentRefUpdateException e) {
throw new CodeReviewNoteCreationException(e);
} finally {
- release();
+ revWalk.release();
+ inserter.release();
}
}
- public void loadBase() throws IOException {
- Ref notesBranch = db.getRef(REFS_NOTES_REVIEW);
- if (notesBranch != null) {
- baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
- base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
- }
- if (baseCommit != null) {
- ours = NoteMap.read(db.newObjectReader(), baseCommit);
- } else {
- ours = NoteMap.newEmptyMap();
- }
- }
+ public void create(List<Change> changes, PersonIdent author,
+ String commitMessage, ProgressMonitor monitor) throws OrmException,
+ IOException, CodeReviewNoteCreationException {
+ try {
+ revWalk = new RevWalk(db);
+ inserter = db.newObjectInserter();
+ if (author != null) {
+ this.author = author;
+ }
+ if (monitor == null) {
+ monitor = NullProgressMonitor.INSTANCE;
+ }
- private void applyNotes() throws IOException, CodeReviewNoteCreationException {
- StringBuilder message =
- new StringBuilder("Update notes for submitted changes\n\n");
- for (CodeReviewCommit c : commits) {
- add(c.change, c);
- message.append("* ").append(c.getShortMessage()).append("\n");
- }
- commit(message.toString());
- }
+ NoteMap notes = NoteMap.newEmptyMap();
+ for (Change c : changes) {
+ monitor.update(1);
+ PatchSet ps = schema.patchSets().get(c.currentPatchSetId());
+ ObjectId commitId = ObjectId.fromString(ps.getRevision().get());
+ notes.set(commitId, createNoteContent(c, commitId));
+ }
- public void commit(String message) throws IOException {
- if (baseCommit != null) {
- oursCommit = createCommit(ours, author, message, baseCommit);
- } else {
- oursCommit = createCommit(ours, author, message);
+ NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
+ notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
+ commitMessage);
+ inserter.flush();
+ } catch (ConcurrentRefUpdateException e) {
+ throw new CodeReviewNoteCreationException(e);
+ } finally {
+ revWalk.release();
+ inserter.release();
}
}
- public void add(Change change, ObjectId commit)
- throws MissingObjectException, IncorrectObjectTypeException, IOException,
- CodeReviewNoteCreationException {
+ private ObjectId createNoteContent(Change change, ObjectId commit)
+ throws CodeReviewNoteCreationException, IOException {
if (!(commit instanceof RevCommit)) {
commit = revWalk.parseCommit(commit);
}
-
- RevCommit c = (RevCommit) commit;
- ObjectId noteContent = createNoteContent(change, c);
- if (ours.contains(c)) {
- // merge the existing and the new note as if they are both new
- // means: base == null
- // there is not really a common ancestry for these two note revisions
- // use the same NoteMerger that is used from the NoteMapMerger
- NoteMerger noteMerger = new ReviewNoteMerger();
- Note newNote = new Note(c, noteContent);
- noteContent = noteMerger.merge(null, newNote, ours.getNote(c),
- reader, inserter).getData();
- }
- ours.set(c, noteContent);
+ return createNoteContent(change, (RevCommit) commit);
}
private ObjectId createNoteContent(Change change, RevCommit commit)
@@ -227,83 +211,4 @@ public class CreateCodeReviewNotes {
throw new CodeReviewNoteCreationException(commit, e);
}
}
-
- public void updateRef() throws IOException, InterruptedException,
- CodeReviewNoteCreationException, MissingObjectException,
- IncorrectObjectTypeException, CorruptObjectException {
- if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
- // If the trees are identical, there is no change in the notes.
- // Avoid saving this commit as it has no new information.
- return;
- }
-
- int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
- RefUpdate refUpdate = createRefUpdate(oursCommit, baseCommit);
-
- for (;;) {
- Result result = refUpdate.update();
-
- if (result == Result.LOCK_FAILURE) {
- if (--remainingLockFailureCalls > 0) {
- Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
- } else {
- throw new CodeReviewNoteCreationException(
- "Failed to lock the ref: " + REFS_NOTES_REVIEW);
- }
-
- } else if (result == Result.REJECTED) {
- RevCommit theirsCommit =
- revWalk.parseCommit(refUpdate.getOldObjectId());
- NoteMap theirs =
- NoteMap.read(revWalk.getObjectReader(), theirsCommit);
- NoteMapMerger merger = new NoteMapMerger(db);
- NoteMap merged = merger.merge(base, ours, theirs);
- RevCommit mergeCommit =
- createCommit(merged, gerritIdent, "Merged note commits\n",
- theirsCommit, oursCommit);
- refUpdate = createRefUpdate(mergeCommit, theirsCommit);
- remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-
- } else if (result == Result.IO_FAILURE) {
- throw new CodeReviewNoteCreationException(
- "Couldn't create code review notes because of IO_FAILURE");
- } else {
- break;
- }
- }
- }
-
- public void release() {
- reader.release();
- inserter.release();
- revWalk.release();
- }
-
- private RevCommit createCommit(NoteMap map, PersonIdent author,
- String message, RevCommit... parents) throws IOException {
- CommitBuilder b = new CommitBuilder();
- b.setTreeId(map.writeTree(inserter));
- b.setAuthor(author != null ? author : gerritIdent);
- b.setCommitter(gerritIdent);
- if (parents.length > 0) {
- b.setParentIds(parents);
- }
- b.setMessage(message);
- ObjectId commitId = inserter.insert(b);
- inserter.flush();
- return revWalk.parseCommit(commitId);
- }
-
-
- private RefUpdate createRefUpdate(ObjectId newObjectId,
- ObjectId expectedOldObjectId) throws IOException {
- RefUpdate refUpdate = db.updateRef(REFS_NOTES_REVIEW);
- refUpdate.setNewObjectId(newObjectId);
- if (expectedOldObjectId == null) {
- refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
- } else {
- refUpdate.setExpectedOldObjectId(expectedOldObjectId);
- }
- return refUpdate;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index a2b04951a6..1bf157b431 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -59,10 +59,11 @@ public interface GitRepositoryManager {
* @return the cached Repository instance. Caller must call {@code close()}
* when done to decrement the resource handle.
* @throws RepositoryNotFoundException the name does not denote an existing
- * repository, or the name cannot be read as a repository.
+ * repository.
+ * @throws IOException the name cannot be read as a repository.
*/
public abstract Repository openRepository(Project.NameKey name)
- throws RepositoryNotFoundException;
+ throws RepositoryNotFoundException, IOException;
/**
* Create (and open) a repository by name.
@@ -73,9 +74,11 @@ public interface GitRepositoryManager {
* @throws RepositoryCaseMismatchException the name collides with an existing
* repository name, but only in case of a character within the name.
* @throws RepositoryNotFoundException the name is invalid.
+ * @throws IOException the repository cannot be created.
*/
public abstract Repository createRepository(Project.NameKey name)
- throws RepositoryCaseMismatchException, RepositoryNotFoundException;
+ throws RepositoryCaseMismatchException, RepositoryNotFoundException,
+ IOException;
/** @return set of all known projects, sorted by natural NameKey order. */
public abstract SortedSet<Project.NameKey> list();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 9fa45e185d..4e3f324925 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -23,6 +23,8 @@ import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.jcraft.jsch.Session;
+
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
@@ -34,6 +36,9 @@ import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.storage.file.LockFile;
import org.eclipse.jgit.storage.file.WindowCache;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
@@ -82,6 +87,15 @@ public class LocalDiskRepositoryManager implements GitRepositoryManager {
@Override
public void start() {
+ // Install our own factory which always runs in batch mode, as we
+ // have no UI available for interactive prompting.
+ SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+ @Override
+ protected void configure(OpenSshConfig.Host hc, Session session) {
+ // Default configuration is batch mode.
+ }
+ });
+
final WindowCacheConfig c = new WindowCacheConfig();
c.fromConfig(cfg);
WindowCache.reconfigure(c);
@@ -122,7 +136,15 @@ public class LocalDiskRepositoryManager implements GitRepositoryManager {
throw new RepositoryNotFoundException("Invalid name: " + name);
}
if (!names.contains(name)) {
- throw new RepositoryNotFoundException(gitDirOf(name));
+ // The this.names list does not hold the project-name but it can still exist
+ // on disk; for instance when the project has been created directly on the
+ // file-system through replication.
+ //
+ if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
+ onCreateProject(name);
+ } else {
+ throw new RepositoryNotFoundException(gitDirOf(name));
+ }
}
final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 44becb525b..1997c13d2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.git;
/** Indicates the current branch's queue cannot be processed at this time. */
-class MergeException extends Exception {
+public class MergeException extends Exception {
private static final long serialVersionUID = 1L;
MergeException(final String msg) {
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 4773680f8e..2e8f183f0d 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
@@ -36,7 +36,9 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -134,7 +136,7 @@ public class MergeOp {
private final SchemaFactory<ReviewDb> schemaFactory;
private final ProjectCache projectCache;
private final FunctionState.Factory functionState;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final MergedSender.Factory mergedSenderFactory;
private final MergeFailSender.Factory mergeFailSenderFactory;
private final Provider<String> urlProvider;
@@ -158,6 +160,7 @@ public class MergeOp {
private CodeReviewCommit mergeTip;
private Set<RevCommit> alreadyAccepted;
private RefUpdate branchUpdate;
+ private ObjectInserter inserter;
private final ChangeHooks hooks;
private final AccountCache accountCache;
@@ -166,11 +169,12 @@ public class MergeOp {
private final SubmoduleOp.Factory subOpFactory;
private final WorkQueue workQueue;
private final RequestScopePropagator requestScopePropagator;
+ private final AllProjectsName allProjectsName;
@Inject
MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
final ProjectCache pc, final FunctionState.Factory fs,
- final ReplicationQueue rq, final MergedSender.Factory msf,
+ final GitReferenceUpdated rq, final MergedSender.Factory msf,
final MergeFailSender.Factory mfsf,
@CanonicalWebUrl @Nullable final Provider<String> cwu,
final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
@@ -182,7 +186,8 @@ public class MergeOp {
final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf,
final SubmoduleOp.Factory subOpFactory,
final WorkQueue workQueue,
- final RequestScopePropagator requestScopePropagator) {
+ final RequestScopePropagator requestScopePropagator,
+ final AllProjectsName allProjectsName) {
repoManager = grm;
schemaFactory = sf;
functionState = fs;
@@ -203,6 +208,7 @@ public class MergeOp {
this.subOpFactory = subOpFactory;
this.workQueue = workQueue;
this.requestScopePropagator = requestScopePropagator;
+ this.allProjectsName = allProjectsName;
this.myIdent = myIdent;
destBranch = branch;
toMerge = new ArrayList<CodeReviewCommit>();
@@ -247,10 +253,12 @@ public class MergeOp {
log.error("Test merge attempt for change: " + change.getId()
+ " failed", e);
} finally {
+ if (repo != null) {
+ repo.close();
+ }
if (db != null) {
db.close();
}
- db = null;
}
}
@@ -281,14 +289,18 @@ public class MergeOp {
} catch (OrmException e) {
throw new MergeException("Cannot query the database", e);
} finally {
+ if (inserter != null) {
+ inserter.release();
+ }
if (rw != null) {
rw.release();
}
if (repo != null) {
repo.close();
}
- db.close();
- db = null;
+ if (db != null) {
+ db.close();
+ }
}
}
@@ -319,6 +331,9 @@ public class MergeOp {
} catch (RepositoryNotFoundException notGit) {
final String m = "Repository \"" + name.get() + "\" unknown.";
throw new MergeException(m, notGit);
+ } catch (IOException err) {
+ final String m = "Error opening repository \"" + name.get() + '"';
+ throw new MergeException(m, err);
}
rw = new RevWalk(repo) {
@@ -330,6 +345,8 @@ public class MergeOp {
rw.sort(RevSort.TOPO);
rw.sort(RevSort.COMMIT_TIME_DESC, true);
CAN_MERGE = rw.newFlag("CAN_MERGE");
+
+ inserter = repo.newObjectInserter();
}
private void openBranch() throws MergeException {
@@ -345,6 +362,21 @@ public class MergeOp {
branchTip = null;
}
+ try {
+ final Ref destRef = repo.getRef(destBranch.get());
+ if (destRef != null) {
+ branchUpdate.setExpectedOldObjectId(destRef.getObjectId());
+ } else if (repo.getFullBranch().equals(destBranch.get())) {
+ branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+ } else {
+ throw new MergeException("Destination branch \""
+ + branchUpdate.getRef().getName() + "\" does not exist");
+ }
+ } catch (IOException e) {
+ throw new MergeException(
+ "Failed to check existence of destination branch", e);
+ }
+
for (final Ref r : repo.getAllRefs().values()) {
if (r.getName().startsWith(Constants.R_HEADS)
|| r.getName().startsWith(Constants.R_TAGS)) {
@@ -423,6 +455,50 @@ public class MergeOp {
continue;
}
+ if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+ final Project.NameKey newParent;
+ try {
+ ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+ cfg.load(repo, commit);
+ newParent = cfg.getProject().getParent(allProjectsName);
+ } catch (Exception e) {
+ commits.put(changeId, CodeReviewCommit
+ .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION));
+ continue;
+ }
+ final Project.NameKey oldParent = destProject.getParent(allProjectsName);
+ if (oldParent == null) {
+ // update of the 'All-Projects' project
+ if (newParent != null) {
+ commits.put(changeId, CodeReviewCommit
+ .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT));
+ continue;
+ }
+ } else {
+ if (!oldParent.equals(newParent)) {
+ final PatchSetApproval psa = getSubmitter(db, ps.getId());
+ if (psa == null) {
+ commits.put(changeId, CodeReviewCommit
+ .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
+ continue;
+ }
+ final IdentifiedUser submitter =
+ identifiedUserFactory.create(psa.getAccountId());
+ if (!submitter.getCapabilities().canAdministrateServer()) {
+ commits.put(changeId, CodeReviewCommit
+ .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
+ continue;
+ }
+
+ if (projectCache.get(newParent) == null) {
+ commits.put(changeId, CodeReviewCommit
+ .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND));
+ continue;
+ }
+ }
+ }
+ }
+
commit.change = chg;
commit.patchsetId = ps.getId();
commit.originalOrder = commitOrder++;
@@ -501,21 +577,10 @@ public class MergeOp {
}
private void mergeOneCommit(final CodeReviewCommit n) throws MergeException {
- final ThreeWayMerger m;
- if (destProject.isUseContentMerge()) {
- // Settings for this project allow us to try and
- // automatically resolve conflicts within files if needed.
- // Use ResolveMerge and instruct to operate in core.
- m = MergeStrategy.RESOLVE.newMerger(repo, true);
- } else {
- // No auto conflict resolving allowed. If any of the
- // affected files was modified, merge will fail.
- m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
- }
-
+ final ThreeWayMerger m = newThreeWayMerger();
try {
if (m.merge(new AnyObjectId[] {mergeTip, n})) {
- writeMergeCommit(m, n);
+ writeMergeCommit(m.getResultTreeId(), n);
} else {
failed(n, CommitMergeStatus.PATH_CONFLICT);
@@ -533,6 +598,35 @@ public class MergeOp {
}
}
+ private ThreeWayMerger newThreeWayMerger() {
+ ThreeWayMerger m;
+ if (destProject.isUseContentMerge()) {
+ // Settings for this project allow us to try and
+ // automatically resolve conflicts within files if needed.
+ // Use ResolveMerge and instruct to operate in core.
+ m = MergeStrategy.RESOLVE.newMerger(repo, true);
+ } else {
+ // No auto conflict resolving allowed. If any of the
+ // affected files was modified, merge will fail.
+ m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+ }
+ m.setObjectInserter(new ObjectInserter.Filter() {
+ @Override
+ protected ObjectInserter delegate() {
+ return inserter;
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void release() {
+ }
+ });
+ return m;
+ }
+
private CodeReviewCommit failed(final CodeReviewCommit n,
final CommitMergeStatus failure) throws MissingObjectException,
IncorrectObjectTypeException, IOException {
@@ -546,7 +640,7 @@ public class MergeOp {
return failed;
}
- private void writeMergeCommit(final Merger m, final CodeReviewCommit n)
+ private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n)
throws IOException, MissingObjectException, IncorrectObjectTypeException {
final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
rw.reset();
@@ -595,13 +689,13 @@ public class MergeOp {
PersonIdent authorIdent = computeAuthor(merged);
final CommitBuilder mergeCommit = new CommitBuilder();
- mergeCommit.setTreeId(m.getResultTreeId());
+ mergeCommit.setTreeId(treeId);
mergeCommit.setParentIds(mergeTip, n);
mergeCommit.setAuthor(authorIdent);
mergeCommit.setCommitter(myIdent);
mergeCommit.setMessage(msgbuf.toString());
- mergeTip = (CodeReviewCommit) rw.parseCommit(commit(m, mergeCommit));
+ mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit));
}
private PersonIdent computeAuthor(
@@ -627,6 +721,12 @@ public class MergeOp {
identifiedUserFactory.create(submitter.getAccountId());
Set<String> emails = new HashSet<String>();
for (RevCommit c : codeReviewCommits) {
+ try {
+ rw.parseBody(c);
+ } catch (IOException e) {
+ log.warn("Cannot parse commit " + c.name() + " in " + destBranch, e);
+ continue;
+ }
emails.add(c.getAuthorIdent().getEmailAddress());
}
@@ -687,19 +787,7 @@ public class MergeOp {
private void cherryPickChanges() throws MergeException, OrmException {
while (!toMerge.isEmpty()) {
final CodeReviewCommit n = toMerge.remove(0);
- final ThreeWayMerger m;
-
- if (destProject.isUseContentMerge()) {
- // Settings for this project allow us to try and
- // automatically resolve conflicts within files if needed.
- // Use ResolveMerge and instruct to operate in core.
- m = MergeStrategy.RESOLVE.newMerger(repo, true);
- } else {
- // No auto conflict resolving allowed. If any of the
- // affected files was modified, merge will fail.
- m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
- }
-
+ final ThreeWayMerger m = newThreeWayMerger();
try {
if (mergeTip == null) {
// The branch is unborn. Take a fast-forward resolution to
@@ -894,42 +982,60 @@ public class MergeOp {
mergeCommit.setCommitter(toCommitterIdent(submitAudit));
mergeCommit.setMessage(msgbuf.toString());
- final ObjectId id = commit(m, mergeCommit);
+ final ObjectId id = commit(mergeCommit);
final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
- n.change =
- db.changes().atomicUpdate(n.change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- change.nextPatchSetId();
- return change;
- }
- });
-
- final PatchSet ps = new PatchSet(n.change.currPatchSetId());
- ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
- ps.setUploader(submitAudit.getAccountId());
- ps.setRevision(new RevId(id.getName()));
- insertAncestors(ps.getId(), newCommit);
- db.patchSets().insert(Collections.singleton(ps));
-
- n.change =
- db.changes().atomicUpdate(n.change.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
- ps.getId()));
- return change;
- }
- });
-
- if (approvalList != null) {
- for (PatchSetApproval a : approvalList) {
- db.patchSetApprovals().insert(
- Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+ if (submitAudit != null) {
+ final Change oldChange = n.change;
+
+ n.change =
+ db.changes().atomicUpdate(n.change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ change.nextPatchSetId();
+ return change;
+ }
+ });
+
+ final PatchSet ps = new PatchSet(n.change.currPatchSetId());
+ ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+ ps.setUploader(submitAudit.getAccountId());
+ ps.setRevision(new RevId(id.getName()));
+ insertAncestors(ps.getId(), newCommit);
+ db.patchSets().insert(Collections.singleton(ps));
+
+ n.change =
+ db.changes().atomicUpdate(n.change.getId(),
+ new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
+ ps.getId()));
+ return change;
+ }
+ });
+
+ this.submitted.remove(oldChange);
+ this.submitted.add(n.change);
+
+ if (approvalList != null) {
+ for (PatchSetApproval a : approvalList) {
+ db.patchSetApprovals().insert(
+ Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+ }
+ }
+
+ final RefUpdate ru = repo.updateRef(ps.getRefName());
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(newCommit);
+ ru.disableRefLog();
+ if (ru.update(rw) != RefUpdate.Result.NEW) {
+ throw new IOException(String.format(
+ "Failed to create ref %s in %s: %s", ps.getRefName(), n.change
+ .getDest().getParentKey().get(), ru.getResult()));
}
+ replication.fire(n.change.getProject(), ru.getName());
}
newCommit.copyFrom(n);
@@ -953,16 +1059,11 @@ public class MergeOp {
db.patchSetAncestors().insert(toInsert);
}
- private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
+ private ObjectId commit(CommitBuilder mergeCommit)
throws IOException, UnsupportedEncodingException {
- ObjectInserter oi = m.getObjectInserter();
- try {
- ObjectId id = oi.insert(mergeCommit);
- oi.flush();
- return id;
- } finally {
- oi.release();
- }
+ ObjectId id = inserter.insert(mergeCommit);
+ inserter.flush();
+ return id;
}
private boolean contains(List<FooterLine> footers, FooterKey key, String val) {
@@ -1026,8 +1127,7 @@ public class MergeOp {
ps.getProject().getDescription());
}
- replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
- .getName());
+ replication.fire(destBranch.getParentKey(), branchUpdate.getName());
Account account = null;
final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
@@ -1096,7 +1196,11 @@ public class MergeOp {
case PATH_CONFLICT:
case CRISS_CROSS_MERGE:
case CANNOT_CHERRY_PICK_ROOT:
- case NOT_FAST_FORWARD: {
+ case NOT_FAST_FORWARD:
+ case INVALID_PROJECT_CONFIGURATION:
+ case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
+ case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
+ case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: {
setNew(c, message(c, txt));
break;
}
@@ -1122,7 +1226,7 @@ public class MergeOp {
} catch (CodeReviewNoteCreationException e) {
log.error(e.getMessage());
}
- replication.scheduleUpdate(destBranch.getParentKey(),
+ replication.fire(destBranch.getParentKey(),
GitRepositoryManager.REFS_NOTES_REVIEW);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index fbe9d16732..86dc19c05f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.git;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -25,6 +26,8 @@ import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import java.io.IOException;
+
/** Helps with the updating of a {@link VersionedMetaData}. */
public class MetaDataUpdate {
public static class User {
@@ -49,7 +52,7 @@ public class MetaDataUpdate {
}
public MetaDataUpdate create(Project.NameKey name)
- throws RepositoryNotFoundException {
+ throws RepositoryNotFoundException, IOException {
MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
md.getCommitBuilder().setAuthor(userIdent);
md.getCommitBuilder().setCommitter(serverIdent);
@@ -71,7 +74,7 @@ public class MetaDataUpdate {
}
public MetaDataUpdate create(Project.NameKey name)
- throws RepositoryNotFoundException {
+ throws RepositoryNotFoundException, IOException {
MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
md.getCommitBuilder().setAuthor(serverIdent);
md.getCommitBuilder().setCommitter(serverIdent);
@@ -84,13 +87,13 @@ public class MetaDataUpdate {
@Assisted Repository db);
}
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final Project.NameKey projectName;
private final Repository db;
private final CommitBuilder commit;
@Inject
- public MetaDataUpdate(ReplicationQueue replication,
+ public MetaDataUpdate(GitReferenceUpdated replication,
@Assisted Project.NameKey projectName, @Assisted Repository db) {
this.replication = replication;
this.projectName = projectName;
@@ -121,8 +124,6 @@ public class MetaDataUpdate {
}
void replicate(String ref) {
- if (replication.isEnabled()) {
- replication.scheduleUpdate(projectName, ref);
- }
+ replication.fire(projectName, ref);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index dbb849c61d..23d8dada7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -17,6 +17,7 @@ package com.google.gerrit.server.git;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ProgressMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,7 +59,7 @@ public class MultiProgressMonitor {
private static final char NO_SPINNER = ' ';
/** Handle for a sub-task. */
- public class Task {
+ public class Task implements ProgressMonitor {
private final String name;
private final int total;
private volatile int count;
@@ -76,6 +77,7 @@ public class MultiProgressMonitor {
*
* @param completed number of work units completed.
*/
+ @Override
public void update(final int completed) {
count += completed;
if (total != UNKNOWN) {
@@ -97,6 +99,23 @@ public class MultiProgressMonitor {
wakeUp();
}
}
+
+ @Override
+ public void start(int totalTasks) {
+ }
+
+ @Override
+ public void beginTask(String title, int totalWork) {
+ }
+
+ @Override
+ public void endTask() {
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
}
private final OutputStream out;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
new file mode 100644
index 0000000000..17cfea8533
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -0,0 +1,271 @@
+// 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.git;
+
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.notes.NoteMapMerger;
+import org.eclipse.jgit.notes.NoteMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+/**
+ * A utility class for updating a notes branch with automatic merge of note
+ * trees.
+ */
+public class NotesBranchUtil {
+ public interface Factory {
+ NotesBranchUtil create(Repository db);
+ }
+
+ private static final int MAX_LOCK_FAILURE_CALLS = 10;
+ private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
+
+ private PersonIdent gerritIdent;
+ private final Repository db;
+
+ private RevCommit baseCommit;
+ private NoteMap base;
+
+ private RevCommit oursCommit;
+ private NoteMap ours;
+
+ private RevWalk revWalk;
+ private ObjectInserter inserter;
+ private ObjectReader reader;
+ private boolean overwrite;
+
+ private ReviewNoteMerger noteMerger;
+
+ @Inject
+ public NotesBranchUtil(@GerritPersonIdent final PersonIdent gerritIdent,
+ @Assisted Repository db) {
+ this.gerritIdent = gerritIdent;
+ this.db = db;
+ }
+
+ /**
+ * Create a new commit in the <code>notesBranch</code> by updating existing
+ * or creating new notes from the <code>notes</code> map.
+ *
+ * @param notes map of notes
+ * @param notesBranch notes branch to update
+ * @param commitAuthor author of the commit in the notes branch
+ * @param commitMessage for the commit in the notes branch
+ * @throws IOException
+ * @throws ConcurrentRefUpdateException
+ */
+ public final void commitAllNotes(NoteMap notes, String notesBranch,
+ PersonIdent commitAuthor, String commitMessage) throws IOException,
+ ConcurrentRefUpdateException {
+ this.overwrite = true;
+ commitNotes(notes, notesBranch, commitAuthor, commitMessage);
+ }
+
+ /**
+ * Create a new commit in the <code>notesBranch</code> by creating not yet
+ * existing notes from the <code>notes</code> map. The notes from the
+ * <code>notes</code> map which already exist in the note-tree of the
+ * tip of the <code>notesBranch</code> will not be updated.
+ *
+ * @param notes map of notes
+ * @param notesBranch notes branch to update
+ * @param commitAuthor author of the commit in the notes branch
+ * @param commitMessage for the commit in the notes branch
+ * @return map with those notes from the <code>notes</code> that were newly
+ * created
+ * @throws IOException
+ * @throws ConcurrentRefUpdateException
+ */
+ public final NoteMap commitNewNotes(NoteMap notes, String notesBranch,
+ PersonIdent commitAuthor, String commitMessage) throws IOException,
+ ConcurrentRefUpdateException {
+ this.overwrite = false;
+ commitNotes(notes, notesBranch, commitAuthor, commitMessage);
+ NoteMap newlyCreated = NoteMap.newEmptyMap();
+ for (Note n : notes) {
+ if (base == null || !base.contains(n)) {
+ newlyCreated.set(n, n.getData());
+ }
+ }
+ return newlyCreated;
+ }
+
+ private void commitNotes(NoteMap notes, String notesBranch,
+ PersonIdent commitAuthor, String commitMessage) throws IOException,
+ ConcurrentRefUpdateException {
+ try {
+ revWalk = new RevWalk(db);
+ inserter = db.newObjectInserter();
+ reader = db.newObjectReader();
+ loadBase(notesBranch);
+ if (overwrite) {
+ addAllNotes(notes);
+ } else {
+ addNewNotes(notes);
+ }
+ if (base != null) {
+ oursCommit = createCommit(ours, commitAuthor, commitMessage, baseCommit);
+ } else {
+ oursCommit = createCommit(ours, commitAuthor, commitMessage);
+ }
+ updateRef(notesBranch);
+ } finally {
+ revWalk.release();
+ inserter.release();
+ reader.release();
+ }
+ }
+
+ private void addNewNotes(NoteMap notes) throws IOException {
+ for (Note n : notes) {
+ if (! ours.contains(n)) {
+ ours.set(n, n.getData());
+ }
+ }
+ }
+
+ private void addAllNotes(NoteMap notes) throws IOException {
+ for (Note n : notes) {
+ if (ours.contains(n)) {
+ // Merge the existing and the new note as if they are both new,
+ // means: base == null
+ // There is no really a common ancestry for these two note revisions
+ ObjectId noteContent = getNoteMerger().merge(null, n, ours.getNote(n),
+ reader, inserter).getData();
+ ours.set(n, noteContent);
+ } else {
+ ours.set(n, n.getData());
+ }
+ }
+ }
+
+ private NoteMerger getNoteMerger() {
+ if (noteMerger == null) {
+ noteMerger = new ReviewNoteMerger();
+ }
+ return noteMerger;
+ }
+
+ private void loadBase(String notesBranch) throws IOException {
+ Ref branch = db.getRef(notesBranch);
+ if (branch != null) {
+ baseCommit = revWalk.parseCommit(branch.getObjectId());
+ base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+ }
+ if (baseCommit != null) {
+ ours = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+ } else {
+ ours = NoteMap.newEmptyMap();
+ }
+ }
+
+ private RevCommit createCommit(NoteMap map, PersonIdent author,
+ String message, RevCommit... parents) throws IOException {
+ CommitBuilder b = new CommitBuilder();
+ b.setTreeId(map.writeTree(inserter));
+ b.setAuthor(author != null ? author : gerritIdent);
+ b.setCommitter(gerritIdent);
+ if (parents.length > 0) {
+ b.setParentIds(parents);
+ }
+ b.setMessage(message);
+ ObjectId commitId = inserter.insert(b);
+ inserter.flush();
+ return revWalk.parseCommit(commitId);
+ }
+
+ private void updateRef(String notesBranch) throws IOException,
+ MissingObjectException, IncorrectObjectTypeException,
+ CorruptObjectException, ConcurrentRefUpdateException {
+ if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
+ // If the trees are identical, there is no change in the notes.
+ // Avoid saving this commit as it has no new information.
+ return;
+ }
+
+ int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+ RefUpdate refUpdate = createRefUpdate(notesBranch, oursCommit, baseCommit);
+
+ for (;;) {
+ Result result = refUpdate.update();
+
+ if (result == Result.LOCK_FAILURE) {
+ if (--remainingLockFailureCalls > 0) {
+ try {
+ Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ } else {
+ throw new ConcurrentRefUpdateException("Failed to lock the ref: "
+ + notesBranch, db.getRef(notesBranch), result);
+ }
+
+ } else if (result == Result.REJECTED) {
+ RevCommit theirsCommit =
+ revWalk.parseCommit(refUpdate.getOldObjectId());
+ NoteMap theirs =
+ NoteMap.read(revWalk.getObjectReader(), theirsCommit);
+ NoteMapMerger merger =
+ new NoteMapMerger(db, getNoteMerger(), MergeStrategy.RESOLVE);
+ NoteMap merged = merger.merge(base, ours, theirs);
+ RevCommit mergeCommit =
+ createCommit(merged, gerritIdent, "Merged note commits\n",
+ theirsCommit, oursCommit);
+ refUpdate = createRefUpdate(notesBranch, mergeCommit, theirsCommit);
+ remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+
+ } else if (result == Result.IO_FAILURE) {
+ throw new IOException("Couldn't update " + notesBranch + ". "
+ + result.name());
+ } else {
+ break;
+ }
+ }
+ }
+
+ private RefUpdate createRefUpdate(String notesBranch, ObjectId newObjectId,
+ ObjectId expectedOldObjectId) throws IOException {
+ RefUpdate refUpdate = db.updateRef(notesBranch);
+ refUpdate.setNewObjectId(newObjectId);
+ if (expectedOldObjectId == null) {
+ refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+ } else {
+ refUpdate.setExpectedOldObjectId(expectedOldObjectId);
+ }
+ return refUpdate;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
new file mode 100644
index 0000000000..ba2833dc8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -0,0 +1,104 @@
+// 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.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.mail.Address;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class NotifyConfig implements Comparable<NotifyConfig> {
+ private String name;
+ private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
+ private String filter;
+
+ private Set<GroupReference> groups = Sets.newHashSet();
+ private Set<Address> addresses = Sets.newHashSet();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isNotify(NotifyType type) {
+ return types.contains(type) || types.contains(NotifyType.ALL);
+ }
+
+ public EnumSet<NotifyType> getNotify() {
+ return types;
+ }
+
+ public void setTypes(EnumSet<NotifyType> newTypes) {
+ types = EnumSet.copyOf(newTypes);
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public void setFilter(String filter) {
+ if ("*".equals(filter)) {
+ this.filter = null;
+ } else {
+ this.filter = Strings.emptyToNull(filter);
+ }
+ }
+
+ public Set<GroupReference> getGroups() {
+ return groups;
+ }
+
+ public Set<Address> getAddresses() {
+ return addresses;
+ }
+
+ public void addEmail(GroupReference group) {
+ groups.add(group);
+ }
+
+ public void addEmail(Address address) {
+ addresses.add(address);
+ }
+
+ @Override
+ public int compareTo(NotifyConfig o) {
+ return name.compareTo(o.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof NotifyConfig) {
+ return compareTo((NotifyConfig) obj) == 0;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 057e80d10a..8aea73ad56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -14,50 +14,86 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.server.RequestCleanup;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.Callable;
-class PerThreadRequestScope {
- static class Propagator
- extends ThreadLocalRequestScopePropagator<PerThreadRequestScope> {
- Propagator() {
- super(REQUEST, current);
+public class PerThreadRequestScope {
+ public interface Scoper {
+ <T> Callable<T> scope(Callable<T> callable);
+ }
+
+ private static class Context {
+ private final Map<Key<?>, Object> map;
+
+ private Context() {
+ map = Maps.newHashMap();
+ }
+
+ private <T> T get(Key<T> key, Provider<T> creator) {
+ @SuppressWarnings("unchecked")
+ T t = (T) map.get(key);
+ if (t == null) {
+ t = creator.get();
+ map.put(key, t);
+ }
+ return t;
+ }
+ }
+
+ public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
+ @Inject
+ Propagator(ThreadLocalRequestContext local) {
+ super(REQUEST, current, local);
}
@Override
- protected PerThreadRequestScope continuingContext(
- PerThreadRequestScope ctx) {
- return new PerThreadRequestScope();
+ protected Context continuingContext(Context ctx) {
+ return new Context();
+ }
+
+ public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) {
+ final Context ctx = new Context();
+ final Callable<T> wrapped = context(requestContext, cleanup(callable));
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ Context old = current.get();
+ current.set(ctx);
+ try {
+ return wrapped.call();
+ } finally {
+ current.set(old);
+ }
+ }
+ };
}
}
- private static final ThreadLocal<PerThreadRequestScope> current =
- new ThreadLocal<PerThreadRequestScope>();
+ private static final ThreadLocal<Context> current = new ThreadLocal<Context>();
- private static PerThreadRequestScope requireContext() {
- final PerThreadRequestScope ctx = current.get();
+ private static Context requireContext() {
+ final Context ctx = current.get();
if (ctx == null) {
throw new OutOfScopeException("Not in command/request");
}
return ctx;
}
- static PerThreadRequestScope set(PerThreadRequestScope ctx) {
- PerThreadRequestScope old = current.get();
- current.set(ctx);
- return old;
- }
-
- static final Scope REQUEST = new Scope() {
+ public static final Scope REQUEST = new Scope() {
+ @Override
public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
return new Provider<T>() {
+ @Override
public T get() {
return requireContext().get(key, creator);
}
@@ -74,26 +110,4 @@ class PerThreadRequestScope {
return "PerThreadRequestScope.REQUEST";
}
};
-
- private static final Key<RequestCleanup> RC_KEY =
- Key.get(RequestCleanup.class);
-
- final RequestCleanup cleanup;
- private final Map<Key<?>, Object> map;
-
- PerThreadRequestScope() {
- cleanup = new RequestCleanup();
- map = new HashMap<Key<?>, Object>();
- map.put(RC_KEY, cleanup);
- }
-
- synchronized <T> T get(Key<T> key, Provider<T> creator) {
- @SuppressWarnings("unchecked")
- T t = (T) map.get(key);
- if (t == null) {
- t = creator.get();
- map.put(key, t);
- }
- return t;
- }
}
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 d15095b271..13e9967f4e 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
@@ -16,22 +16,31 @@ package com.google.gerrit.server.git;
import static com.google.gerrit.common.data.Permission.isPermission;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
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.common.data.RefConfigSection;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.State;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.mail.Address;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -39,6 +48,7 @@ import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -56,6 +66,20 @@ public class ProjectConfig extends VersionedMetaData {
private static final String KEY_INHERIT_FROM = "inheritFrom";
private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
+ private static final String ACCOUNTS = "accounts";
+ private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";
+
+ private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
+ private static final String KEY_ACCEPTED = "accepted";
+ private static final String KEY_REQUIRE_CONTACT_INFORMATION = "requireContactInformation";
+ private static final String KEY_AUTO_VERIFY = "autoVerify";
+ private static final String KEY_AGREEMENT_URL = "agreementUrl";
+
+ private static final String NOTIFY = "notify";
+ private static final String KEY_EMAIL = "email";
+ private static final String KEY_FILTER = "filter";
+ private static final String KEY_TYPE = "type";
+
private static final String CAPABILITY = "capability";
private static final String RECEIVE = "receive";
@@ -76,8 +100,11 @@ public class ProjectConfig extends VersionedMetaData {
private Project.NameKey projectName;
private Project project;
+ private AccountsSection accountsSection;
private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
private Map<String, AccessSection> accessSections;
+ private Map<String, ContributorAgreement> contributorAgreements;
+ private Map<String, NotifyConfig> notifySections;
private List<ValidationError> validationErrors;
private ObjectId rulesId;
@@ -103,6 +130,10 @@ public class ProjectConfig extends VersionedMetaData {
return project;
}
+ public AccountsSection getAccountsSection() {
+ return accountsSection;
+ }
+
public AccessSection getAccessSection(String name) {
return getAccessSection(name, false);
}
@@ -136,6 +167,42 @@ public class ProjectConfig extends VersionedMetaData {
accessSections.put(section.getName(), section);
}
+ public ContributorAgreement getContributorAgreement(String name) {
+ return getContributorAgreement(name, false);
+ }
+
+ public ContributorAgreement getContributorAgreement(String name, boolean create) {
+ ContributorAgreement ca = contributorAgreements.get(name);
+ if (ca == null && create) {
+ ca = new ContributorAgreement(name);
+ contributorAgreements.put(name, ca);
+ }
+ return ca;
+ }
+
+ public Collection<ContributorAgreement> getContributorAgreements() {
+ return sort(contributorAgreements.values());
+ }
+
+ public void remove(ContributorAgreement section) {
+ if (section != null) {
+ accessSections.remove(section.getName());
+ }
+ }
+
+ public void replace(ContributorAgreement section) {
+ section.setAutoVerify(resolve(section.getAutoVerify()));
+ for (PermissionRule rule : section.getAccepted()) {
+ rule.setGroup(resolve(rule.getGroup()));
+ }
+
+ contributorAgreements.put(section.getName(), section);
+ }
+
+ public Collection<NotifyConfig> getNotifyConfigs() {
+ return notifySections.values();
+ }
+
public GroupReference resolve(AccountGroup group) {
return resolve(GroupReference.forGroup(group));
}
@@ -167,13 +234,13 @@ public class ProjectConfig extends VersionedMetaData {
/**
* Check all GroupReferences use current group name, repairing stale ones.
*
- * @param groupCache cache to use when looking up group information by UUID.
+ * @param groupBackend cache to use when looking up group information by UUID.
* @return true if one or more group names was stale.
*/
- public boolean updateGroupNames(GroupCache groupCache) {
+ public boolean updateGroupNames(GroupBackend groupBackend) {
boolean dirty = false;
for (GroupReference ref : groupsByUUID.values()) {
- AccountGroup g = groupCache.get(ref.getUUID());
+ GroupDescription.Basic g = groupBackend.get(ref.getUUID());
if (g != null && !g.getName().equals(ref.getName())) {
dirty = true;
ref.setName(g.getName());
@@ -223,6 +290,116 @@ public class ProjectConfig extends VersionedMetaData {
p.setUseContentMerge(getBoolean(rc, SUBMIT, KEY_MERGE_CONTENT, false));
p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
+ loadAccountsSection(rc, groupsByName);
+ loadContributorAgreements(rc, groupsByName);
+ loadAccessSections(rc, groupsByName);
+ loadNotifySections(rc, groupsByName);
+ }
+
+ private void loadAccountsSection(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ accountsSection = new AccountsSection();
+ accountsSection.setSameGroupVisibility(loadPermissionRules(
+ rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false));
+ }
+
+ private void loadContributorAgreements(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ contributorAgreements = new HashMap<String, ContributorAgreement>();
+ for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
+ ContributorAgreement ca = getContributorAgreement(name, true);
+ ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
+ ca.setRequireContactInformation(
+ rc.getBoolean(CONTRIBUTOR_AGREEMENT, name, KEY_REQUIRE_CONTACT_INFORMATION, false));
+ ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
+ ca.setAccepted(loadPermissionRules(
+ rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, groupsByName, false));
+
+ List<PermissionRule> rules = loadPermissionRules(
+ rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, groupsByName, false);
+ if (rules.isEmpty()) {
+ ca.setAutoVerify(null);
+ } else if (rules.size() > 1) {
+ error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "." + name
+ + "." + KEY_AUTO_VERIFY
+ + ": at most one group may be set"));
+ } else if (rules.get(0).getAction() != Action.ALLOW) {
+ error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "." + name
+ + "." + KEY_AUTO_VERIFY
+ + ": the group must be allowed"));
+ } else {
+ ca.setAutoVerify(rules.get(0).getGroup());
+ }
+ }
+ }
+
+ /**
+ * Parses the [notify] sections out of the configuration file.
+ *
+ * <pre>
+ * [notify "reviewers"]
+ * email = group Reviewers
+ * type = new_changes
+ *
+ * [notify "dev-team"]
+ * email = dev-team@example.com
+ * filter = branch:master
+ *
+ * [notify "qa"]
+ * email = qa@example.com
+ * filter = branch:\"^(maint|stable)-.*\"
+ * type = submitted_changes
+ * </pre>
+ */
+ private void loadNotifySections(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ notifySections = Maps.newHashMap();
+ for (String sectionName : rc.getSubsections(NOTIFY)) {
+ NotifyConfig n = new NotifyConfig();
+ n.setName(sectionName);
+ n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+ EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+ types.addAll(ConfigUtil.getEnumList(rc,
+ NOTIFY, sectionName, KEY_TYPE,
+ NotifyType.ALL));
+ n.setTypes(types);
+
+ for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
+ if (dst.startsWith("group ")) {
+ String groupName = dst.substring(6).trim();
+ GroupReference ref = groupsByName.get(groupName);
+ if (ref == null) {
+ ref = new GroupReference(null, groupName);
+ groupsByName.put(ref.getName(), ref);
+ }
+ if (ref.getUUID() != null) {
+ n.addEmail(ref);
+ } else {
+ error(new ValidationError(PROJECT_CONFIG,
+ "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+ }
+ } else if (dst.startsWith("user ")) {
+ error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+ } else {
+ try {
+ n.addEmail(Address.parse(dst));
+ } catch (IllegalArgumentException err) {
+ error(new ValidationError(PROJECT_CONFIG,
+ "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+ }
+ }
+ }
+ notifySections.put(sectionName, n);
+ }
+ }
+
+ private void loadAccessSections(
+ Config rc, Map<String, GroupReference> groupsByName) {
accessSections = new HashMap<String, AccessSection>();
for (String refName : rc.getSubsections(ACCESS)) {
if (RefConfigSection.isValid(refName)) {
@@ -260,6 +437,15 @@ public class ProjectConfig extends VersionedMetaData {
}
}
+ private List<PermissionRule> loadPermissionRules(Config rc, String section,
+ String subsection, String varName,
+ Map<String, GroupReference> groupsByName,
+ boolean useRange) {
+ Permission perm = new Permission(varName);
+ loadPermissionRules(rc, section, subsection, varName, groupsByName, perm, useRange);
+ return perm.getRules();
+ }
+
private void loadPermissionRules(Config rc, String section,
String subsection, String varName,
Map<String, GroupReference> groupsByName, Permission perm,
@@ -349,6 +535,100 @@ public class ProjectConfig extends VersionedMetaData {
set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
+ saveAccountsSection(rc, keepGroups);
+ saveContributorAgreements(rc, keepGroups);
+ saveAccessSections(rc, keepGroups);
+ saveNotifySections(rc, keepGroups);
+ groupsByUUID.keySet().retainAll(keepGroups);
+
+ saveConfig(PROJECT_CONFIG, rc);
+ saveGroupList();
+ }
+
+ private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
+ if (accountsSection != null) {
+ rc.setStringList(ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY,
+ ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups));
+ }
+ }
+
+ private void saveContributorAgreements(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (ContributorAgreement ca : sort(contributorAgreements.values())) {
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_REQUIRE_CONTACT_INFORMATION, ca.isRequireContactInformation());
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
+
+ if (ca.getAutoVerify() != null) {
+ if (ca.getAutoVerify().getUUID() != null) {
+ keepGroups.add(ca.getAutoVerify().getUUID());
+ }
+ String autoVerify = new PermissionRule(ca.getAutoVerify()).asString(false);
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY, autoVerify);
+ } else {
+ rc.unset(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY);
+ }
+
+ rc.setStringList(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_ACCEPTED,
+ ruleToStringList(ca.getAccepted(), keepGroups));
+ }
+ }
+
+ private void saveNotifySections(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (NotifyConfig nc : sort(notifySections.values())) {
+ List<String> email = Lists.newArrayList();
+ for (GroupReference gr : nc.getGroups()) {
+ if (gr.getUUID() != null) {
+ keepGroups.add(gr.getUUID());
+ }
+ email.add(new PermissionRule(gr).asString(false));
+ }
+ Collections.sort(email);
+
+ List<String> addrs = Lists.newArrayList();
+ for (Address addr : nc.getAddresses()) {
+ addrs.add(addr.toString());
+ }
+ Collections.sort(addrs);
+ email.addAll(addrs);
+
+ if (email.isEmpty()) {
+ rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
+ } else {
+ rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
+ }
+
+ if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+ rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+ } else {
+ List<String> types = Lists.newArrayListWithCapacity(4);
+ for (NotifyType t : NotifyType.values()) {
+ if (nc.isNotify(t)) {
+ types.add(StringUtils.toLowerCase(t.name()));
+ }
+ }
+ rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
+ }
+
+ set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
+ }
+ }
+
+ private List<String> ruleToStringList(
+ List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
+ List<String> rules = new ArrayList<String>();
+ for (PermissionRule rule : sort(list)) {
+ if (rule.getGroup().getUUID() != null) {
+ keepGroups.add(rule.getGroup().getUUID());
+ }
+ rules.add(rule.asString(false));
+ }
+ return rules;
+ }
+
+ private void saveAccessSections(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
if (capability != null) {
Set<String> have = new HashSet<String>();
@@ -425,10 +705,6 @@ public class ProjectConfig extends VersionedMetaData {
rc.unsetSection(ACCESS, name);
}
}
- groupsByUUID.keySet().retainAll(keepGroups);
-
- saveConfig(PROJECT_CONFIG, rc);
- saveGroupList();
}
private void saveGroupList() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
deleted file mode 100644
index 845d037f6e..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-
-public class PushAllProjectsOp extends DefaultQueueOp {
- public interface Factory {
- PushAllProjectsOp create(String urlMatch);
- }
-
- private static final Logger log =
- LoggerFactory.getLogger(PushAllProjectsOp.class);
-
- private final ProjectCache projectCache;
- private final ReplicationQueue replication;
- private final String urlMatch;
-
- @Inject
- public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
- final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
- super(wq);
- this.projectCache = projectCache;
- this.replication = rq;
- this.urlMatch = urlMatch;
- }
-
- @Override
- public void start(final int delay, final TimeUnit unit) {
- if (replication.isEnabled()) {
- super.start(delay, unit);
- }
- }
-
- public void run() {
- try {
- for (final Project.NameKey nameKey : projectCache.all()) {
- replication.scheduleFullSync(nameKey, urlMatch);
- }
- } catch (RuntimeException e) {
- log.error("Cannot enumerate known projects", e);
- }
- }
-
- @Override
- public String toString() {
- String s = "Replicate All Projects";
- if (urlMatch != null) {
- s = s + " to " + urlMatch;
- }
- return s;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
deleted file mode 100644
index 08687490ff..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ /dev/null
@@ -1,433 +0,0 @@
-// 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.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.errors.NoRemoteRepositoryException;
-import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchConnection;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
-import org.slf4j.Logger;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A push to remote operation started by {@link ReplicationQueue}.
- * <p>
- * Instance members are protected by the lock within PushQueue. Callers must
- * take that lock to ensure they are working with a current view of the object.
- */
-class PushOp implements ProjectRunnable {
- interface Factory {
- PushOp create(Project.NameKey d, URIish u);
- }
-
- private static final Logger log = PushReplication.log;
- static final String ALL_REFS = "..all..";
-
- private final GitRepositoryManager repoManager;
- private final SchemaFactory<ReviewDb> schema;
- private final PushReplication.ReplicationConfig pool;
- private final RemoteConfig config;
- private final CredentialsProvider credentialsProvider;
- private final TagCache tagCache;
-
- private final Set<String> delta = new HashSet<String>();
- private final Project.NameKey projectName;
- private final URIish uri;
- private boolean pushAllRefs;
-
- private Repository db;
-
- /**
- * It indicates if the current instance is in fact retrying to push.
- */
- private boolean retrying;
-
- private boolean canceled;
-
- @Inject
- PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
- final PushReplication.ReplicationConfig p, final RemoteConfig c,
- final SecureCredentialsProvider.Factory cpFactory,
- final TagCache tc,
- @Assisted final Project.NameKey d, @Assisted final URIish u) {
- repoManager = grm;
- schema = s;
- pool = p;
- config = c;
- credentialsProvider = cpFactory.create(c.getName());
- tagCache = tc;
- projectName = d;
- uri = u;
- }
-
- public boolean isRetrying() {
- return retrying;
- }
-
- public void setToRetry() {
- retrying = true;
- }
-
- public void cancel() {
- canceled = true;
- }
-
- public boolean wasCanceled() {
- return canceled;
- }
-
- URIish getURI() {
- return uri;
- }
-
- void addRef(final String ref) {
- if (ALL_REFS.equals(ref)) {
- delta.clear();
- pushAllRefs = true;
- } else if (!pushAllRefs) {
- delta.add(ref);
- }
- }
-
- public Set<String> getRefs() {
- final Set<String> refs;
-
- if (pushAllRefs) {
- refs = new HashSet<String>(1);
- refs.add(ALL_REFS);
- } else {
- refs = delta;
- }
-
- return refs;
- }
-
- public void addRefs(Set<String> refs) {
- if (!pushAllRefs) {
- for (String ref : refs) {
- addRef(ref);
- }
- }
- }
-
- public void run() {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- runPushOperation();
- } finally {
- PerThreadRequestScope.set(old);
- }
- }
-
- private void runPushOperation() {
- // Lock the queue, and remove ourselves, so we can't be modified once
- // we start replication (instead a new instance, with the same URI, is
- // created and scheduled for a future point in time.)
- //
- pool.notifyStarting(this);
-
- // It should only verify if it was canceled after calling notifyStarting,
- // since the canceled flag would be set locking the queue.
- if (!canceled) {
- try {
- db = repoManager.openRepository(projectName);
- runImpl();
- } catch (RepositoryNotFoundException e) {
- log.error("Cannot replicate " + projectName + "; " + e.getMessage());
-
- } catch (NoRemoteRepositoryException e) {
- log.error("Cannot replicate to " + uri + "; repository not found");
-
- } catch (NotSupportedException e) {
- log.error("Cannot replicate to " + uri, e);
-
- } catch (TransportException e) {
- final Throwable cause = e.getCause();
- if (cause instanceof JSchException
- && cause.getMessage().startsWith("UnknownHostKey:")) {
- log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
- } else {
- log.error("Cannot replicate to " + uri, e);
- }
-
- // The remote push operation should be retried.
- pool.reschedule(this);
- } catch (IOException e) {
- log.error("Cannot replicate to " + uri, e);
-
- } catch (RuntimeException e) {
- log.error("Unexpected error during replication to " + uri, e);
-
- } catch (Error e) {
- log.error("Unexpected error during replication to " + uri, e);
-
- } finally {
- if (db != null) {
- db.close();
- }
- }
- }
- }
-
- @Override
- public String toString() {
- return "push " + uri;
- }
-
- private void runImpl() throws IOException {
- final Transport tn = Transport.open(db, uri);
- final PushResult res;
- try {
- res = pushVia(tn);
- } finally {
- try {
- tn.close();
- } catch (Throwable e2) {
- log.warn("Unexpected error while closing " + uri, e2);
- }
- }
-
- for (final RemoteRefUpdate u : res.getRemoteUpdates()) {
- switch (u.getStatus()) {
- case OK:
- case UP_TO_DATE:
- case NON_EXISTING:
- break;
-
- case NOT_ATTEMPTED:
- case AWAITING_REPORT:
- case REJECTED_NODELETE:
- case REJECTED_NONFASTFORWARD:
- case REJECTED_REMOTE_CHANGED:
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ": status " + u.getStatus().name());
- break;
-
- case REJECTED_OTHER_REASON:
- if ("non-fast-forward".equals(u.getMessage())) {
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ", remote rejected non-fast-forward push."
- + " Check receive.denyNonFastForwards variable in config file"
- + " of destination repository.");
- } else {
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ", reason: " + u.getMessage());
- }
- break;
- }
- }
- }
-
- private PushResult pushVia(final Transport tn) throws IOException,
- NotSupportedException, TransportException {
- tn.applyConfig(config);
- tn.setCredentialsProvider(credentialsProvider);
-
- final List<RemoteRefUpdate> todo = generateUpdates(tn);
- if (todo.isEmpty()) {
- // If we have no commands selected, we have nothing to do.
- // Calling JGit at this point would just redo the work we
- // already did, and come up with the same answer. Instead
- // send back an empty result.
- //
- return new PushResult();
- }
-
- return tn.push(NullProgressMonitor.INSTANCE, todo);
- }
-
- private List<RemoteRefUpdate> generateUpdates(final Transport tn)
- throws IOException {
- final ProjectControl pc;
- try {
- pc = pool.controlFor(projectName);
- } catch (NoSuchProjectException e) {
- return Collections.emptyList();
- }
-
- Map<String, Ref> local = db.getAllRefs();
- if (!pc.allRefsAreVisible()) {
- if (!pushAllRefs) {
- // If we aren't mirroring, reduce the space we need to filter
- // to only the references we will update during this operation.
- //
- Map<String, Ref> n = new HashMap<String, Ref>();
- for (String src : delta) {
- Ref r = local.get(src);
- if (r != null) {
- n.put(src, r);
- }
- }
- local = n;
- }
-
- final ReviewDb meta;
- try {
- meta = schema.open();
- } catch (OrmException e) {
- log.error("Cannot read database to replicate to " + projectName, e);
- return Collections.emptyList();
- }
- try {
- local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local, true);
- } finally {
- meta.close();
- }
- }
-
- final boolean noPerms = !pool.isReplicatePermissions();
- final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
- if (pushAllRefs) {
- final Map<String, Ref> remote = listRemote(tn);
-
- for (final Ref src : local.values()) {
- if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src.getName())) {
- continue;
- }
-
- final RefSpec spec = matchSrc(src.getName());
- if (spec != null) {
- final Ref dst = remote.get(spec.getDestination());
- if (dst == null || !src.getObjectId().equals(dst.getObjectId())) {
- // Doesn't exist yet, or isn't the same value, request to push.
- //
- send(cmds, spec, src);
- }
- }
- }
-
- if (config.isMirror()) {
- for (final Ref ref : remote.values()) {
- if (!Constants.HEAD.equals(ref.getName())) {
- final RefSpec spec = matchDst(ref.getName());
- if (spec != null && !local.containsKey(spec.getSource())) {
- // No longer on local side, request removal.
- //
- delete(cmds, spec);
- }
- }
- }
- }
-
- } else {
- for (final String src : delta) {
- final RefSpec spec = matchSrc(src);
- if (spec != null) {
- // If the ref still exists locally, send it, otherwise delete it.
- //
- Ref srcRef = local.get(src);
- if (srcRef != null &&
- !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
- send(cmds, spec, srcRef);
- } else if (config.isMirror()) {
- delete(cmds, spec);
- }
- }
- }
- }
-
- return cmds;
- }
-
- private Map<String, Ref> listRemote(final Transport tn)
- throws NotSupportedException, TransportException {
- final FetchConnection fc = tn.openFetch();
- try {
- return fc.getRefsMap();
- } finally {
- fc.close();
- }
- }
-
- private RefSpec matchSrc(final String ref) {
- for (final RefSpec s : config.getPushRefSpecs()) {
- if (s.matchSource(ref)) {
- return s.expandFromSource(ref);
- }
- }
- return null;
- }
-
- private RefSpec matchDst(final String ref) {
- for (final RefSpec s : config.getPushRefSpecs()) {
- if (s.matchDestination(ref)) {
- return s.expandFromDestination(ref);
- }
- }
- return null;
- }
-
- private void send(final List<RemoteRefUpdate> cmds, final RefSpec spec,
- final Ref src) throws IOException {
- final String dst = spec.getDestination();
- final boolean force = spec.isForceUpdate();
- cmds.add(new RemoteRefUpdate(db, src, dst, force, null, null));
- }
-
- private void delete(final List<RemoteRefUpdate> cmds, final RefSpec spec)
- throws IOException {
- final String dst = spec.getDestination();
- final boolean force = spec.isForceUpdate();
- cmds.add(new RemoteRefUpdate(db, (Ref) null, dst, force, null, null));
- }
-
- @Override
- public NameKey getProjectNameKey() {
- return projectName;
- }
-
- @Override
- public String getRemoteName() {
- return config.getName();
- }
-
- @Override
- public boolean hasCustomizedPrint() {
- return true;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
deleted file mode 100644
index 5cf5f7a32e..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ /dev/null
@@ -1,668 +0,0 @@
-// 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.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.RequestScoped;
-
-import com.jcraft.jsch.Session;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.FileRepository;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteSession;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.io.StreamCopyThread;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-/** Manages automatic replication to remote repositories. */
-@Singleton
-public class PushReplication implements ReplicationQueue {
- static final Logger log = LoggerFactory.getLogger(PushReplication.class);
-
- public static class Module extends AbstractModule {
- @Override
- protected void configure() {
- bind(ReplicationQueue.class).to(PushReplication.class);
- }
- }
-
- static {
- // Install our own factory which always runs in batch mode, as we
- // have no UI available for interactive prompting.
- //
- SshSessionFactory.setInstance(new JschConfigSessionFactory() {
- @Override
- protected void configure(OpenSshConfig.Host hc, Session session) {
- // Default configuration is batch mode.
- }
- });
- }
-
- private final Injector injector;
- private final WorkQueue workQueue;
- private final List<ReplicationConfig> configs;
- private final SchemaFactory<ReviewDb> database;
- private final ReplicationUser.Factory replicationUserFactory;
- private final GitRepositoryManager gitRepositoryManager;
-
- @Inject
- PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
- final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
- final GitRepositoryManager grm)
- throws ConfigInvalidException, IOException {
- injector = i;
- workQueue = wq;
- database = db;
- replicationUserFactory = ruf;
- gitRepositoryManager = grm;
- configs = allConfigs(site);
- }
-
- @Override
- public boolean isEnabled() {
- return configs.size() > 0;
- }
-
- @Override
- public void scheduleFullSync(final Project.NameKey project,
- final String urlMatch) {
- for (final ReplicationConfig cfg : configs) {
- for (final URIish uri : cfg.getURIs(project, urlMatch)) {
- cfg.schedule(project, PushOp.ALL_REFS, uri);
- }
- }
- }
-
- @Override
- public void scheduleUpdate(final Project.NameKey project, final String ref) {
- for (final ReplicationConfig cfg : configs) {
- if (cfg.wouldPushRef(ref)) {
- for (final URIish uri : cfg.getURIs(project, null)) {
- cfg.schedule(project, ref, uri);
- }
- }
- }
- }
-
- private static String replace(final String pat, final String key,
- final String val) {
- final int n = pat.indexOf("${" + key + "}");
-
- if (n != -1) {
- return pat.substring(0, n) + val + pat.substring(n + 3 + key.length());
- } else {
- return null;
- }
- }
-
- private List<ReplicationConfig> allConfigs(final SitePaths site)
- throws ConfigInvalidException, IOException {
- final FileBasedConfig cfg =
- new FileBasedConfig(site.replication_config, FS.DETECTED);
-
- if (!cfg.getFile().exists()) {
- log.warn("No " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
- if (cfg.getFile().length() == 0) {
- log.info("Empty " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
-
- try {
- cfg.load();
- } catch (ConfigInvalidException e) {
- throw new ConfigInvalidException("Config file " + cfg.getFile()
- + " is invalid: " + e.getMessage(), e);
- } catch (IOException e) {
- throw new IOException("Cannot read " + cfg.getFile() + ": "
- + e.getMessage(), e);
- }
-
- final List<ReplicationConfig> r = new ArrayList<ReplicationConfig>();
- for (final RemoteConfig c : allRemotes(cfg)) {
- if (c.getURIs().isEmpty()) {
- continue;
- }
-
- for (final URIish u : c.getURIs()) {
- if (u.getPath() == null || !u.getPath().contains("${name}")) {
- throw new ConfigInvalidException("remote." + c.getName() + ".url"
- + " \"" + u + "\" lacks ${name} placeholder in " + cfg.getFile());
- }
- }
-
- // In case if refspec destination for push is not set then we assume it is
- // equal to source
- for (RefSpec ref : c.getPushRefSpecs()) {
- if (ref.getDestination() == null) {
- ref.setDestination(ref.getSource());
- }
- }
-
-
- if (c.getPushRefSpecs().isEmpty()) {
- RefSpec spec = new RefSpec();
- spec = spec.setSourceDestination("refs/*", "refs/*");
- spec = spec.setForceUpdate(true);
- c.addPushRefSpec(spec);
- }
-
- r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
- replicationUserFactory, gitRepositoryManager));
- }
- return Collections.unmodifiableList(r);
- }
-
- private List<RemoteConfig> allRemotes(final FileBasedConfig cfg)
- throws ConfigInvalidException {
- List<String> names = new ArrayList<String>(cfg.getSubsections("remote"));
- Collections.sort(names);
-
- final List<RemoteConfig> result = new ArrayList<RemoteConfig>(names.size());
- for (final String name : names) {
- try {
- result.add(new RemoteConfig(cfg, name));
- } catch (URISyntaxException e) {
- throw new ConfigInvalidException("remote " + name
- + " has invalid URL in " + cfg.getFile());
- }
- }
- return result;
- }
-
- @Override
- public void replicateNewProject(Project.NameKey projectName, String head) {
- if (!isEnabled()) {
- return;
- }
-
- for (ReplicationConfig config : configs) {
- List<URIish> uriList = config.getURIs(projectName, "*");
- String[] adminUrls = config.getAdminUrls();
- boolean adminURLUsed = false;
-
- for (String url : adminUrls) {
- URIish adminURI = null;
- try {
- if (url != null && !url.isEmpty()) {
- adminURI = new URIish(url);
- }
- } catch (URISyntaxException e) {
- log.error("The URL '" + url + "' is invalid");
- }
-
- if (adminURI != null) {
- final String replacedPath =
- replace(adminURI.getPath(), "name", projectName.get());
- if (replacedPath != null) {
- adminURI = adminURI.setPath(replacedPath);
- if (usingSSH(adminURI)) {
- replicateProject(adminURI, head);
- adminURLUsed = true;
- } else {
- log.error("The adminURL '" + url
- + "' is non-SSH which is not allowed");
- }
- }
- }
- }
-
- if (!adminURLUsed) {
- for (URIish uri : uriList) {
- replicateProject(uri, head);
- }
- }
- }
- }
-
- private void replicateProject(final URIish replicateURI, final String head) {
- if (!replicateURI.isRemote()) {
- replicateProjectLocally(replicateURI, head);
- } else if (usingSSH(replicateURI)) {
- replicateProjectOverSsh(replicateURI, head);
- } else {
- log.warn("Cannot create new project on remote site since neither the "
- + "connection method is SSH nor the replication target is local: "
- + replicateURI.toString());
- return;
- }
- }
-
- private void replicateProjectLocally(final URIish replicateURI,
- final String head) {
- try {
- final Repository repo = new FileRepository(replicateURI.getPath());
- try {
- repo.create(true /* bare */);
-
- final RefUpdate u = repo.updateRef(Constants.HEAD);
- u.disableRefLog();
- u.link(head);
- } finally {
- repo.close();
- }
- } catch (IOException e) {
- log.error("Failed to replicate project locally: "
- + replicateURI.getPath());
- }
- }
-
- private void replicateProjectOverSsh(final URIish replicateURI,
- final String head) {
- SshSessionFactory sshFactory = SshSessionFactory.getInstance();
- RemoteSession sshSession;
- String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath());
-
- OutputStream errStream = createErrStream();
- String cmd =
- "mkdir -p " + projectPath + "&& cd " + projectPath
- + "&& git init --bare" + "&& git symbolic-ref HEAD "
- + QuotedString.BOURNE.quote(head);
-
- try {
- sshSession = sshFactory.getSession(replicateURI, null, FS.DETECTED, 0);
- Process proc = sshSession.exec(cmd, 0);
- proc.getOutputStream().close();
- StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream);
- StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream);
- out.start();
- err.start();
- try {
- proc.waitFor();
- out.halt();
- err.halt();
- } catch (InterruptedException interrupted) {
- // Don't wait, drop out immediately.
- }
- sshSession.disconnect();
- } catch (IOException e) {
- log.error("Communication error when trying to replicate to: "
- + replicateURI.toString() + "\n" + "Error reported: "
- + e.getMessage() + "\n" + "Error in communication: "
- + errStream.toString());
- }
- }
-
- private OutputStream createErrStream() {
- return new OutputStream() {
- private StringBuilder all = new StringBuilder();
- private StringBuilder sb = new StringBuilder();
-
- @Override
- public String toString() {
- String r = all.toString();
- while (r.endsWith("\n"))
- r = r.substring(0, r.length() - 1);
- return r;
- }
-
- @Override
- public synchronized void write(final int b) {
- if (b == '\r') {
- return;
- }
-
- sb.append((char) b);
-
- if (b == '\n') {
- all.append(sb);
- sb.setLength(0);
- }
- }
- };
- }
-
- private boolean usingSSH(final URIish uri) {
- final String scheme = uri.getScheme();
- if (!uri.isRemote()) return false;
- if (scheme != null && scheme.toLowerCase().contains("ssh")) return true;
- if (scheme == null && uri.getHost() != null && uri.getPath() != null)
- return true;
- return false;
- }
-
- static class ReplicationConfig {
- private final RemoteConfig remote;
- private final String[] adminUrls;
- private final int delay;
- private final int retryDelay;
- private final WorkQueue.Executor pool;
- private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
- private final PushOp.Factory opFactory;
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager mgr;
- private final boolean replicatePermissions;
-
- ReplicationConfig(final Injector injector, final WorkQueue workQueue,
- final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
- final ReplicationUser.Factory replicationUserFactory,
- final GitRepositoryManager gitRepositoryManager) {
-
- remote = rc;
- delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
- retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
-
- final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1));
- final String poolName = "ReplicateTo-" + rc.getName();
- pool = workQueue.createQueue(poolSize, poolName);
-
- String[] authGroupNames =
- cfg.getStringList("remote", rc.getName(), "authGroup");
- final GroupMembership authGroups;
- if (authGroupNames.length > 0) {
- authGroups = new ListGroupMembership(ConfigUtil.groupsFor(db, authGroupNames, //
- log, "Group \"{0}\" not in database, removing from authGroup"));
- } else {
- authGroups = ReplicationUser.EVERYTHING_VISIBLE;
- }
-
- adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
- replicatePermissions = cfg.getBoolean("remote", rc.getName(),
- "replicatePermissions", true);
- mgr = gitRepositoryManager;
-
- final ReplicationUser remoteUser =
- replicationUserFactory.create(authGroups);
-
- projectControlFactory =
- injector.createChildInjector(new AbstractModule() {
- @Override
- protected void configure() {
- bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
- bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
- bind(CurrentUser.class).toInstance(remoteUser);
- }
- }).getInstance(ProjectControl.Factory.class);
-
- opFactory = injector.createChildInjector(new FactoryModule() {
- @Override
- protected void configure() {
- bind(PushReplication.ReplicationConfig.class).toInstance(ReplicationConfig.this);
- bind(RemoteConfig.class).toInstance(remote);
- factory(PushOp.Factory.class);
- }
- }).getInstance(PushOp.Factory.class);
- }
-
- private int getInt(final RemoteConfig rc, final Config cfg,
- final String name, final int defValue) {
- return cfg.getInt("remote", rc.getName(), name, defValue);
- }
-
- void schedule(final Project.NameKey project, final String ref,
- final URIish uri) {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- try {
- if (!controlFor(project).isVisible()) {
- return;
- }
- } catch (NoSuchProjectException e1) {
- log.error("Internal error: project " + project
- + " not found during replication");
- return;
- }
- } finally {
- PerThreadRequestScope.set(old);
- }
-
- if (!replicatePermissions) {
- PushOp e;
- synchronized (pending) {
- e = pending.get(uri);
- }
- if (e == null) {
- Repository git;
- try {
- git = mgr.openRepository(project);
- } catch (RepositoryNotFoundException err) {
- log.error("Internal error: project " + project
- + " not found during replication", err);
- return;
- }
- try {
- Ref head = git.getRef(Constants.HEAD);
- if (head != null
- && head.isSymbolic()
- && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
- return;
- }
- } catch (IOException err) {
- log.error("Internal error: cannot check type of project " + project
- + " during replication", err);
- return;
- } finally {
- git.close();
- }
- }
- }
-
- synchronized (pending) {
- PushOp e = pending.get(uri);
- if (e == null) {
- e = opFactory.create(project, uri);
- pool.schedule(e, delay, TimeUnit.SECONDS);
- pending.put(uri, e);
- }
- e.addRef(ref);
- }
- }
-
- /**
- * It schedules again a PushOp instance.
- * <p>
- * It is assumed to be previously scheduled and found a
- * transport exception. It will schedule it as a push
- * operation to be retried after the minutes count
- * determined by class attribute retryDelay.
- * <p>
- * In case the PushOp instance to be scheduled has same
- * URI than one also pending for retry, it adds to the one
- * pending the refs list of the parameter instance.
- * <p>
- * In case the PushOp instance to be scheduled has same
- * URI than one pending, but not pending for retry, it
- * indicates the one pending should be canceled when it
- * starts executing, removes it from pending list, and
- * adds its refs to the parameter instance. The parameter
- * instance is scheduled for retry.
- * <p>
- * Notice all operations to indicate a PushOp should be
- * canceled, or it is retrying, or remove/add it from/to
- * pending Map should be protected by the lock on pending
- * Map class instance attribute.
- *
- * @param pushOp The PushOp instance to be scheduled.
- */
- void reschedule(final PushOp pushOp) {
- // It locks access to pending variable.
- synchronized (pending) {
- URIish uri = pushOp.getURI();
- PushOp pendingPushOp = pending.get(uri);
-
- if (pendingPushOp != null) {
- // There is one PushOp instance already pending to same URI.
-
- if (pendingPushOp.isRetrying()) {
- // The one pending is one already retrying, so it should
- // maintain it and add to it the refs of the one passed
- // as parameter to the method.
-
- // This scenario would happen if a PushOp has started running
- // and then before it failed due transport exception, another
- // one to same URI started. The first one would fail and would
- // be rescheduled, being present in pending list. When the
- // second one fails, it will also be rescheduled and then,
- // here, find out replication to its URI is already pending
- // for retry (blocking).
- pendingPushOp.addRefs(pushOp.getRefs());
-
- } else {
- // The one pending is one that is NOT retrying, it was just
- // scheduled believing no problem would happen. The one pending
- // should be canceled, and this is done by setting its canceled
- // flag, removing it from pending list, and adding its refs to
- // the pushOp instance that should then, later, in this method,
- // be scheduled for retry.
-
- // Notice that the PushOp found pending will start running and,
- // when notifying it is starting (with pending lock protection),
- // it will see it was canceled and then it will do nothing with
- // pending list and it will not execute its run implementation.
-
- pendingPushOp.cancel();
- pending.remove(uri);
-
- pushOp.addRefs(pendingPushOp.getRefs());
- }
- }
-
- if (pendingPushOp == null || !pendingPushOp.isRetrying()) {
- // The PushOp method param instance should be scheduled for retry.
- // Remember when retrying it should be used different delay.
-
- pushOp.setToRetry();
-
- pending.put(uri, pushOp);
- pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES);
- }
- }
- }
-
- ProjectControl controlFor(final Project.NameKey project)
- throws NoSuchProjectException {
- return projectControlFactory.controlFor(project);
- }
-
- void notifyStarting(final PushOp op) {
- synchronized (pending) {
- if (!op.wasCanceled()) {
- pending.remove(op.getURI());
- }
- }
- }
-
- boolean wouldPushRef(final String ref) {
- if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
- return false;
- }
- for (final RefSpec s : remote.getPushRefSpecs()) {
- if (s.matchSource(ref)) {
- return true;
- }
- }
- return false;
- }
-
- boolean isReplicatePermissions() {
- return replicatePermissions;
- }
-
- List<URIish> getURIs(final Project.NameKey project, final String urlMatch) {
- final List<URIish> r = new ArrayList<URIish>(remote.getURIs().size());
- for (URIish uri : remote.getURIs()) {
- if (matches(uri, urlMatch)) {
- String name = project.get();
- if (needsUrlEncoding(uri)) {
- name = encode(name);
- }
- String replacedPath = replace(uri.getPath(), "name", name);
- if (replacedPath != null) {
- uri = uri.setPath(replacedPath);
- r.add(uri);
- }
- }
- }
- return r;
- }
-
- static boolean needsUrlEncoding(URIish uri) {
- return "http".equalsIgnoreCase(uri.getScheme())
- || "https".equalsIgnoreCase(uri.getScheme())
- || "amazon-s3".equalsIgnoreCase(uri.getScheme());
- }
-
- static String encode(String str) {
- try {
- // Some cleanup is required. The '/' character is always encoded as %2F
- // however remote servers will expect it to be not encoded as part of the
- // path used to the repository. Space is incorrectly encoded as '+' for this
- // context. In the path part of a URI space should be %20, but in form data
- // space is '+'. Our cleanup replace fixes these two issues.
- return URLEncoder.encode(str, "UTF-8")
- .replaceAll("%2[fF]", "/")
- .replace("+", "%20");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
- }
-
- String[] getAdminUrls() {
- return this.adminUrls;
- }
-
- private boolean matches(URIish uri, final String urlMatch) {
- if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
- return true;
- }
- return uri.toString().contains(urlMatch);
- }
- }
-}
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 37fbdb24ea..a2a809bc06 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
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -21,15 +22,20 @@ import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import com.google.common.base.Predicate;
+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.Sets;
+import com.google.common.util.concurrent.CheckedFuture;
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.NoSuchAccountException;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -45,8 +51,10 @@ import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergedSender;
@@ -61,18 +69,19 @@ import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
@@ -85,6 +94,7 @@ import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
import org.eclipse.jgit.transport.ReceivePack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -116,6 +126,32 @@ public class ReceiveCommits {
private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+ private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
+ "Please read the documentation and contact an administrator\n"
+ + "if you feel the configuration is incorrect";
+
+ private enum Error {
+ CONFIG_UPDATE("You are not allowed to perform this operation.\n"
+ + "Configuration changes can only be pushed by project owners\n"
+ + "who also have 'Push' rights on " + GitRepositoryManager.REF_CONFIG),
+ UPDATE("You are not allowed to perform this operation.\n"
+ + "To push into this reference you need 'Push' rights."),
+ DELETE("You need 'Push' rights with the 'Force Push'\n"
+ + "flag set to delete references."),
+ CODE_REVIEW("You need 'Push' rights to upload code review requests.\n"
+ + "Verify that you are pushing to the right branch.");
+
+ private final String value;
+
+ Error(String value) {
+ this.value = value;
+ }
+
+ public String get() {
+ return value;
+ }
+ }
+
interface Factory {
ReceiveCommits create(ProjectControl projectControl, Repository repository);
}
@@ -178,14 +214,14 @@ public class ReceiveCommits {
private final IdentifiedUser currentUser;
private final ReviewDb db;
- private final ApprovalTypes approvalTypes;
private final AccountResolver accountResolver;
private final CreateChangeSender.Factory createChangeSenderFactory;
private final MergedSender.Factory mergedSenderFactory;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSetInfoFactory patchSetInfoFactory;
private final ChangeHooks hooks;
+ private final ApprovalsUtil approvalsUtil;
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
private final String canonicalWebUrl;
@@ -194,6 +230,7 @@ public class ReceiveCommits {
private final TagCache tagCache;
private final WorkQueue workQueue;
private final RequestScopePropagator requestScopePropagator;
+ private final AllProjectsName allProjectsName;
private final ProjectControl projectControl;
private final Project project;
@@ -204,13 +241,12 @@ public class ReceiveCommits {
private Branch.NameKey destBranch;
private RefControl destBranchCtl;
- private final List<Change> allNewChanges = new ArrayList<Change>();
+ private List<CreateRequest> newChanges = Collections.emptyList();
private final Map<Change.Id, ReplaceRequest> replaceByChange =
new HashMap<Change.Id, ReplaceRequest>();
private final Map<RevCommit, ReplaceRequest> replaceByCommit =
new HashMap<RevCommit, ReplaceRequest>();
- private Collection<ObjectId> existingObjects;
private Map<ObjectId, Ref> refsById;
private String destTopicName;
@@ -218,21 +254,24 @@ public class ReceiveCommits {
private final SubmoduleOp.Factory subOpFactory;
private final List<Message> messages = new ArrayList<Message>();
+ private ListMultimap<Error, String> errors = LinkedListMultimap.create();
private Task newProgress;
private Task replaceProgress;
private Task closeProgress;
private Task commandProgress;
private MessageSender messageSender;
+ private BatchRefUpdate batch;
@Inject
- ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
+ ReceiveCommits(final ReviewDb db,
final AccountResolver accountResolver,
final CreateChangeSender.Factory createChangeSenderFactory,
final MergedSender.Factory mergedSenderFactory,
final ReplacePatchSetSender.Factory replacePatchSetFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
final PatchSetInfoFactory patchSetInfoFactory,
final ChangeHooks hooks,
+ final ApprovalsUtil approvalsUtil,
final ProjectCache projectCache,
final GitRepositoryManager repoManager,
final TagCache tagCache,
@@ -241,13 +280,13 @@ public class ReceiveCommits {
final TrackingFooters trackingFooters,
final WorkQueue workQueue,
final RequestScopePropagator requestScopePropagator,
+ final AllProjectsName allProjectsName,
@Assisted final ProjectControl projectControl,
@Assisted final Repository repo,
final SubmoduleOp.Factory subOpFactory) throws IOException {
this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
this.db = db;
- this.approvalTypes = approvalTypes;
this.accountResolver = accountResolver;
this.createChangeSenderFactory = createChangeSenderFactory;
this.mergedSenderFactory = mergedSenderFactory;
@@ -255,6 +294,7 @@ public class ReceiveCommits {
this.replication = replication;
this.patchSetInfoFactory = patchSetInfoFactory;
this.hooks = hooks;
+ this.approvalsUtil = approvalsUtil;
this.projectCache = projectCache;
this.repoManager = repoManager;
this.canonicalWebUrl = canonicalWebUrl;
@@ -263,6 +303,7 @@ public class ReceiveCommits {
this.tagCache = tagCache;
this.workQueue = workQueue;
this.requestScopePropagator = requestScopePropagator;
+ this.allProjectsName = allProjectsName;
this.projectControl = projectControl;
this.project = projectControl.getProject();
@@ -429,15 +470,44 @@ public class ReceiveCommits {
closeProgress = progress.beginSubTask("closed", UNKNOWN);
commandProgress = progress.beginSubTask("refs", UNKNOWN);
+ batch = repo.getRefDatabase().newBatchUpdate();
+ batch.setRefLogIdent(rp.getRefLogIdent());
+ batch.setRefLogMessage("push", true);
+
parseCommands(commands);
if (newChange != null && newChange.getResult() == NOT_ATTEMPTED) {
- createNewChanges();
+ newChanges = selectNewChanges();
+ }
+ preparePatchSetsForReplace();
+
+ if (!batch.getCommands().isEmpty()) {
+ try {
+ batch.execute(rp.getRevWalk(), commandProgress);
+ } catch (IOException err) {
+ int cnt = 0;
+ for (ReceiveCommand cmd : batch.getCommands()) {
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
+ cnt++;
+ }
+ }
+ log.error(String.format(
+ "Failed to store %d refs in %s", cnt, project.getName()), err);
+ }
}
- newProgress.end();
- doReplaces();
+ insertChangesAndPatchSets();
+ newProgress.end();
replaceProgress.end();
+ if (!errors.isEmpty()) {
+ for (Error error : errors.keySet()) {
+ rp.sendMessage(buildError(error, errors.get(error)));
+ }
+ rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+ rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+ }
+
for (final ReceiveCommand c : commands) {
if (c.getResult() == OK) {
switch (c.getType()) {
@@ -475,10 +545,12 @@ public class ReceiveCommits {
// We only schedule direct refs updates for replication.
// Change refs are scheduled when they are created.
//
- replication.scheduleUpdate(project.getNameKey(), c.getRefName());
- Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
- hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
- commandProgress.update(1);
+ replication.fire(project.getNameKey(), c.getRefName());
+ hooks.doRefUpdatedHook(
+ new Branch.NameKey(project.getNameKey(), c.getRefName()),
+ c.getOldId(),
+ c.getNewId(),
+ currentUser.getAccount());
}
}
}
@@ -486,22 +558,129 @@ public class ReceiveCommits {
commandProgress.end();
progress.end();
- if (!allNewChanges.isEmpty() && canonicalWebUrl != null) {
+ Iterable<CreateRequest> created =
+ Iterables.filter(newChanges, new Predicate<CreateRequest>() {
+ @Override
+ public boolean apply(CreateRequest input) {
+ return input.created;
+ }
+ });
+ if (!Iterables.isEmpty(created) && canonicalWebUrl != null) {
final String url = canonicalWebUrl;
addMessage("");
addMessage("New Changes:");
- for (final Change c : allNewChanges) {
- if (c.getStatus() == Change.Status.DRAFT) {
- addMessage(" " + url + c.getChangeId() + " [DRAFT]");
- }
- else {
- addMessage(" " + url + c.getChangeId());
+ for (CreateRequest c : created) {
+ StringBuilder m = new StringBuilder()
+ .append(" ")
+ .append(url)
+ .append(c.change.getChangeId());
+ if (c.change.getStatus() == Change.Status.DRAFT) {
+ m.append(" [DRAFT]");
}
+ addMessage(m.toString());
}
addMessage("");
}
}
+ private void insertChangesAndPatchSets() {
+ int replaceCount = 0;
+ int okToInsert = 0;
+
+ for (ReplaceRequest replace : replaceByChange.values()) {
+ if (replace.inputCommand == newChange) {
+ replaceCount++;
+
+ if (replace.cmd != null && replace.cmd.getResult() == OK) {
+ okToInsert++;
+ }
+ } else if (replace.cmd != null && replace.cmd.getResult() == OK) {
+ try {
+ if (replace.insertPatchSet() != null) {
+ replace.inputCommand.setResult(OK);
+ }
+ } catch (IOException err) {
+ reject(replace.inputCommand, "internal server error");
+ log.error(String.format(
+ "Cannot add patch set to %d of %s",
+ replace.newPatchSet.getId(), project.getName()), err);
+ } catch (OrmException err) {
+ reject(replace.inputCommand, "internal server error");
+ log.error(String.format(
+ "Cannot add patch set to %d of %s",
+ replace.newPatchSet.getId(), project.getName()), err);
+ }
+ } else {
+ reject(replace.inputCommand, "internal server error");
+ }
+ }
+
+ if (newChange == null || newChange.getResult() != NOT_ATTEMPTED) {
+ // refs/for/ or refs/drafts/ not used, or it already failed earlier.
+ // No need to continue.
+ return;
+ }
+
+ for (CreateRequest create : newChanges) {
+ if (create.cmd.getResult() == OK) {
+ okToInsert++;
+ }
+ }
+
+ if (okToInsert != replaceCount + newChanges.size()) {
+ // One or more new references failed to create. Assume the
+ // system isn't working correctly anymore and abort.
+ reject(newChange, "internal server error");
+ log.error(String.format(
+ "Only %d of %d new change refs created in %s; aborting",
+ okToInsert, newChanges.size(), project.getName()));
+ return;
+ }
+
+ try {
+ for (ReplaceRequest replace : replaceByChange.values()) {
+ if (replace.inputCommand == newChange) {
+ replace.insertPatchSet();
+ }
+ }
+
+ for (CreateRequest create : newChanges) {
+ create.insertChange();
+ }
+ newChange.setResult(OK);
+ } catch (OrmException err) {
+ log.error("Can't insert changes for " + project.getName(), err);
+ reject(newChange, "internal server error");
+ } catch (IOException err) {
+ log.error("Can't read commits for " + project.getName(), err);
+ reject(newChange, "internal server error");
+ }
+ }
+
+ private String buildError(Error error, List<String> branches) {
+ StringBuilder sb = new StringBuilder();
+ if (branches.size() == 1) {
+ sb.append("Branch ").append(branches.get(0)).append(":\n");
+ sb.append(error.get());
+ return sb.toString();
+ }
+ sb.append("Branches");
+ String delim = " ";
+ for (String branch : branches) {
+ sb.append(delim).append(branch);
+ delim = ", ";
+ }
+ return sb.append(":\n").append(error.get()).toString();
+ }
+
+ private static String displayName(IdentifiedUser user) {
+ String displayName = user.getUserName();
+ if (displayName == null) {
+ displayName = user.getAccount().getPreferredEmail();
+ }
+ return displayName;
+ }
+
private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
NoSuchAccountException {
final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
@@ -590,6 +769,26 @@ public class ReceiveCommits {
+ cmd.getNewId().name() + " for " + project.getName());
continue;
}
+ Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
+ Project.NameKey oldParent = project.getParent(allProjectsName);
+ if (oldParent == null) {
+ // update of the 'All-Projects' project
+ if (newParent != null) {
+ reject(cmd, "invalid project configuration: root project cannot have parent");
+ continue;
+ }
+ } else {
+ if (!oldParent.equals(newParent)
+ && !currentUser.getCapabilities().canAdministrateServer()) {
+ reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
+ continue;
+ }
+
+ if (projectCache.get(newParent) == null) {
+ reject(cmd, "invalid project configuration: parent does not exist");
+ continue;
+ }
+ }
} catch (Exception e) {
reject(cmd, "invalid project configuration");
log.error("User " + currentUser.getUserName()
@@ -628,11 +827,9 @@ public class ReceiveCommits {
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canCreate(rp.getRevWalk(), obj)) {
validateNewCommits(ctl, cmd);
- if (cmd.getResult() == NOT_ATTEMPTED) {
- cmd.execute(rp);
- }
+ batch.addCommand(cmd);
} else {
- reject(cmd, "can not create new references");
+ reject(cmd);
}
}
@@ -644,11 +841,14 @@ public class ReceiveCommits {
}
validateNewCommits(ctl, cmd);
- if (cmd.getResult() == NOT_ATTEMPTED) {
- cmd.execute(rp);
- }
+ batch.addCommand(cmd);
} else {
- reject(cmd, "can not update the reference as a fast forward");
+ if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+ errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG);
+ } else {
+ errors.put(Error.UPDATE, ctl.getRefName());
+ }
+ reject(cmd);
}
}
@@ -674,11 +874,14 @@ public class ReceiveCommits {
private void parseDelete(final ReceiveCommand cmd) {
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canDelete()) {
- if (cmd.getResult() == NOT_ATTEMPTED) {
- cmd.execute(rp);
- }
+ batch.addCommand(cmd);
} else {
- reject(cmd, "can not delete references");
+ if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+ reject(cmd, "cannot delete project configuration");
+ } else {
+ errors.put(Error.DELETE, ctl.getRefName());
+ reject(cmd, "can not delete references");
+ }
}
}
@@ -704,9 +907,7 @@ public class ReceiveCommits {
}
if (ctl.canForceUpdate()) {
- if (cmd.getResult() == NOT_ATTEMPTED) {
- cmd.execute(rp);
- }
+ batch.setAllowNonFastForwards(true).addCommand(cmd);
} else {
cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
+ PermissionRule.FORCE_PUSH + "' privilege.");
@@ -777,7 +978,8 @@ public class ReceiveCommits {
destBranchName.substring(0, split));
destBranchCtl = projectControl.controlForRef(destBranch);
if (!destBranchCtl.canUpload()) {
- reject(cmd, "can not upload a change to this reference");
+ errors.put(Error.CODE_REVIEW, cmd.getRefName());
+ reject(cmd, "can not upload review");
return;
}
@@ -901,29 +1103,25 @@ public class ReceiveCommits {
return true;
}
- private void createNewChanges() {
- final List<RevCommit> toCreate = new ArrayList<RevCommit>();
+ private List<CreateRequest> selectNewChanges() {
+ final List<CreateRequest> newChanges = Lists.newArrayList();
final RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
try {
+ Set<ObjectId> existing = Sets.newHashSet();
walk.markStart(walk.parseCommit(newChange.getNewId()));
- for (ObjectId id : existingObjects()) {
- try {
- walk.markUninteresting(walk.parseCommit(id));
- } catch (IOException e) {
- continue;
- }
- }
+ markHeadsAsUninteresting(walk, existing);
+ List<ChangeLookup> pending = Lists.newArrayList();
final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
for (;;) {
final RevCommit c = walk.next();
if (c == null) {
break;
}
- if (replaceByCommit.containsKey(c)) {
+ if (existing.contains(c) || replaceByCommit.containsKey(c)) {
// This commit was already scheduled to replace an existing PatchSet.
//
continue;
@@ -931,59 +1129,63 @@ public class ReceiveCommits {
if (!validCommit(destBranchCtl, newChange, c)) {
// Not a change the user can propose? Abort as early as possible.
//
- return;
+ return Collections.emptyList();
}
+ Change.Key changeKey = new Change.Key("I" + c.name());
final List<String> idList = c.getFooterLines(CHANGE_ID);
- if (!idList.isEmpty()) {
- final String idStr = idList.get(idList.size() - 1).trim();
- if (idStr.matches("^I00*$")) {
- // Reject this invalid line from EGit.
- reject(newChange, "invalid Change-Id");
- return;
- }
+ if (idList.isEmpty()) {
+ newChanges.add(new CreateRequest(c, changeKey));
+ continue;
+ }
- final Change.Key key = new Change.Key(idStr);
+ final String idStr = idList.get(idList.size() - 1).trim();
+ if (idStr.matches("^I00*$")) {
+ // Reject this invalid line from EGit.
+ reject(newChange, "invalid Change-Id");
+ return Collections.emptyList();
+ }
- if (newChangeIds.contains(key)) {
- reject(newChange, "squash commits first");
- return;
- }
+ changeKey = new Change.Key(idStr);
+ pending.add(new ChangeLookup(c, changeKey));
+ }
- final List<Change> changes =
- db.changes().byBranchKey(destBranch, key).toList();
- if (changes.size() > 1) {
- // WTF, multiple changes in this project have the same key?
- // Since the commit is new, the user should recreate it with
- // a different Change-Id. In practice, we should never see
- // this error message as Change-Id should be unique.
- //
- reject(newChange, key.get() + " has duplicates");
- return;
+ for (ChangeLookup p : pending) {
+ if (newChangeIds.contains(p.changeKey)) {
+ reject(newChange, "squash commits first");
+ return Collections.emptyList();
+ }
- }
+ List<Change> changes = p.changes.toList();
+ if (changes.size() > 1) {
+ // WTF, multiple changes in this project have the same key?
+ // Since the commit is new, the user should recreate it with
+ // a different Change-Id. In practice, we should never see
+ // this error message as Change-Id should be unique.
+ //
+ reject(newChange, p.changeKey.get() + " has duplicates");
+ return Collections.emptyList();
+ }
- if (changes.size() == 1) {
- // Schedule as a replacement to this one matching change.
- //
- if (requestReplace(newChange, false, changes.get(0), c)) {
- continue;
- } else {
- return;
- }
+ if (changes.size() == 1) {
+ // Schedule as a replacement to this one matching change.
+ //
+ if (requestReplace(newChange, false, changes.get(0), p.commit)) {
+ continue;
+ } else {
+ return Collections.emptyList();
}
+ }
- if (changes.size() == 0) {
- if (!isValidChangeId(idStr)) {
- reject(newChange, "invalid Change-Id");
- return;
- }
-
- newChangeIds.add(key);
+ if (changes.size() == 0) {
+ if (!isValidChangeId(p.changeKey.get())) {
+ reject(newChange, "invalid Change-Id");
+ return Collections.emptyList();
}
- }
- toCreate.add(c);
+ newChangeIds.add(p.changeKey);
+ }
+ newChanges.add(new CreateRequest(p.commit, p.changeKey));
}
} catch (IOException e) {
// Should never happen, the core receive process would have
@@ -991,159 +1193,158 @@ public class ReceiveCommits {
//
newChange.setResult(REJECTED_MISSING_OBJECT);
log.error("Invalid pack upload; one or more objects weren't sent", e);
- return;
+ return Collections.emptyList();
} catch (OrmException e) {
log.error("Cannot query database to locate prior changes", e);
reject(newChange, "database error");
- return;
+ return Collections.emptyList();
}
- if (toCreate.isEmpty() && replaceByChange.isEmpty()) {
+ if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
reject(newChange, "no new changes");
- return;
+ return Collections.emptyList();
}
+ for (CreateRequest create : newChanges) {
+ batch.addCommand(create.cmd);
+ }
+ return newChanges;
+ }
- for (final RevCommit c : toCreate) {
- try {
- createChange(walk, c);
- } catch (IOException e) {
- log.error("Error computing patch of commit " + c.name(), e);
- reject(newChange, "diff error");
- return;
- } catch (OrmException e) {
- log.error("Error creating change for commit " + c.name(), e);
- reject(newChange, "database error");
- return;
+
+ private void markHeadsAsUninteresting(final RevWalk walk, Set<ObjectId> existing) {
+ for (Ref ref : repo.getAllRefs().values()) {
+ if (ref.getObjectId() == null) {
+ continue;
+ } else if (ref.getName().startsWith("refs/changes/")) {
+ existing.add(ref.getObjectId());
+ } else if (ref.getName().startsWith(R_HEADS)
+ || (destBranchCtl != null && ref.getName().equals(destBranchCtl.getRefName()))) {
+ try {
+ walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
+ } catch (IOException e) {
+ log.warn(String.format("Invalid ref %s in %s",
+ ref.getName(), project.getName()), e);
+ continue;
+ }
}
- newProgress.update(1);
}
- newChange.setResult(OK);
}
private static boolean isValidChangeId(String idStr) {
return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
}
- private void createChange(final RevWalk walk, final RevCommit c)
- throws OrmException, IOException {
- walk.parseBody(c);
- warnMalformedMessage(c);
+ private class ChangeLookup {
+ final RevCommit commit;
+ final Change.Key changeKey;
+ final ResultSet<Change> changes;
- final Account.Id me = currentUser.getAccountId();
- Change.Key changeKey = new Change.Key("I" + c.name());
- final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
- final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
- final List<FooterLine> footerLines = c.getFooterLines();
- for (final FooterLine footerLine : footerLines) {
- try {
- if (footerLine.matches(CHANGE_ID)) {
- final String v = footerLine.getValue().trim();
- if (isValidChangeId(v)) {
- changeKey = new Change.Key(v);
- }
- } else if (isReviewer(footerLine)) {
- reviewers.add(toAccountId(footerLine.getValue().trim()));
- } else if (footerLine.matches(FooterKey.CC)) {
- cc.add(toAccountId(footerLine.getValue().trim()));
- }
- } catch (NoSuchAccountException e) {
- continue;
- }
+ ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
+ commit = c;
+ changeKey = key;
+ changes = db.changes().byBranchKey(destBranch, key);
}
- reviewers.remove(me);
- cc.remove(me);
- cc.removeAll(reviewers);
+ }
+ private class CreateRequest {
+ final RevCommit commit;
final Change change;
final PatchSet ps;
- final PatchSetInfo info;
+ final ReceiveCommand cmd;
+ private final PatchSetInfo info;
+ boolean created;
- change = new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
- change.setTopic(destTopicName);
- change.nextPatchSetId();
+ CreateRequest(RevCommit c, Change.Key changeKey) throws OrmException {
+ commit = c;
+
+ change = new Change(changeKey,
+ new Change.Id(db.nextChangeId()),
+ currentUser.getAccountId(),
+ destBranch);
+ change.setTopic(destTopicName);
+ change.nextPatchSetId();
- db.changes().beginTransaction(change.getId());
- try {
ps = new PatchSet(change.currPatchSetId());
ps.setCreatedOn(change.getCreatedOn());
- ps.setUploader(me);
+ ps.setUploader(change.getOwner());
ps.setRevision(toRevId(c));
+
if (MagicBranch.isDraft(newChange.getRefName())) {
change.setStatus(Change.Status.DRAFT);
ps.setDraft(true);
}
- insertAncestors(ps.getId(), c);
- db.patchSets().insert(Collections.singleton(ps));
info = patchSetInfoFactory.get(c, ps.getId());
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
- db.changes().insert(Collections.singleton(change));
- ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-
- final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
- final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
- haveApprovals.add(me);
-
- if (allTypes.size() > 0) {
- final Account.Id authorId =
- info.getAuthor() != null ? info.getAuthor().getAccount() : null;
- final Account.Id committerId =
- info.getCommitter() != null ? info.getCommitter().getAccount() : null;
- final ApprovalCategory.Id catId =
- allTypes.get(allTypes.size() - 1).getCategory().getId();
- if (authorId != null && haveApprovals.add(authorId)) {
- insertDummyApproval(change, ps.getId(), authorId, catId, db);
- }
- if (committerId != null && haveApprovals.add(committerId)) {
- insertDummyApproval(change, ps.getId(), committerId, catId, db);
- }
- for (final Account.Id reviewer : reviewers) {
- if (haveApprovals.add(reviewer)) {
- insertDummyApproval(change, ps.getId(), reviewer, catId, db);
- }
- }
- }
- db.commit();
- } finally {
- db.rollback();
+ cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName());
}
- final RefUpdate ru = repo.updateRef(ps.getRefName());
- ru.setNewObjectId(c);
- ru.disableRefLog();
- if (ru.update(walk) != RefUpdate.Result.NEW) {
- throw new IOException("Failed to create ref " + ps.getRefName() + " in "
- + repo.getDirectory() + ": " + ru.getResult());
- }
- replication.scheduleUpdate(project.getNameKey(), ru.getName());
+ void insertChange() throws IOException, OrmException {
+ rp.getRevWalk().parseBody(commit);
+ warnMalformedMessage(commit);
- allNewChanges.add(change);
-
- workQueue.getDefaultQueue()
- .submit(requestScopePropagator.wrap(new Runnable() {
- @Override
- public void run() {
+ final Account.Id me = currentUser.getAccountId();
+ final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
+ final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
+ final List<FooterLine> footerLines = commit.getFooterLines();
+ for (final FooterLine footerLine : footerLines) {
try {
- final CreateChangeSender cm;
- cm = createChangeSenderFactory.create(change);
- cm.setFrom(me);
- cm.setPatchSet(ps, info);
- cm.addReviewers(reviewers);
- cm.addExtraCC(cc);
- cm.send();
- } catch (Exception e) {
- log.error("Cannot send email for new change " + change.getId(), e);
+ if (ps.isDraft()) {
+ continue;
+ }
+ if (isReviewer(footerLine)) {
+ reviewers.add(toAccountId(footerLine.getValue().trim()));
+ } else if (footerLine.matches(FooterKey.CC)) {
+ cc.add(toAccountId(footerLine.getValue().trim()));
+ }
+ } catch (NoSuchAccountException e) {
+ continue;
}
}
+ reviewers.remove(me);
+ cc.remove(me);
+ cc.removeAll(reviewers);
- @Override
- public String toString() {
- return "send-email newchange";
+ db.changes().beginTransaction(change.getId());
+ try {
+ insertAncestors(ps.getId(), commit);
+ db.patchSets().insert(Collections.singleton(ps));
+ db.changes().insert(Collections.singleton(change));
+ ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+ approvalsUtil.addReviewers(change, ps, info, reviewers);
+ db.commit();
+ } finally {
+ db.rollback();
}
- }));
- hooks.doPatchsetCreatedHook(change, ps, db);
+ created = true;
+ replication.fire(project.getNameKey(), ps.getRefName());
+ hooks.doPatchsetCreatedHook(change, ps, db);
+ newProgress.update(1);
+ workQueue.getDefaultQueue()
+ .submit(requestScopePropagator.wrap(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ CreateChangeSender cm =
+ createChangeSenderFactory.create(change);
+ cm.setFrom(me);
+ cm.setPatchSet(ps, info);
+ cm.addReviewers(reviewers);
+ cm.addExtraCC(cc);
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new change " + change.getId(), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "send-email newchange";
+ }
+ }));
+ }
}
private static boolean isReviewer(final FooterLine candidateFooterLine) {
@@ -1153,349 +1354,385 @@ public class ReceiveCommits {
|| candidateFooterLine.matches(TESTED_BY);
}
- private void doReplaces() {
- for (final ReplaceRequest request : replaceByChange.values()) {
- try {
- doReplace(request, false);
- replaceProgress.update(1);
- } catch (IOException err) {
- log.error("Error computing replacement patch for change "
- + request.ontoChange + ", commit " + request.newCommit.name(), err);
- reject(request.cmd, "diff error");
- } catch (OrmException err) {
- log.error("Error storing replacement patch for change "
- + request.ontoChange + ", commit " + request.newCommit.name(), err);
- reject(request.cmd, "database error");
+ private void preparePatchSetsForReplace() {
+ try {
+ readChangesForReplace();
+ readPatchSetsForReplace();
+
+ for (ReplaceRequest req : replaceByChange.values()) {
+ if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+ req.validate(false);
+ }
}
- if (request.cmd.getResult() == NOT_ATTEMPTED) {
- log.error("Replacement patch for change " + request.ontoChange
- + ", commit " + request.newCommit.name() + " wasn't attempted."
- + " This is a bug in the receive process implementation.");
- reject(request.cmd, "internal error");
+ } catch (OrmException err) {
+ log.error("Cannot read database before replacement", err);
+ for (ReplaceRequest req : replaceByChange.values()) {
+ if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+ req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
+ }
+ }
+ } catch (IOException err) {
+ log.error("Cannot read repository before replacement", err);
+ for (ReplaceRequest req : replaceByChange.values()) {
+ if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+ req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
+ }
}
}
- }
- private PatchSet.Id doReplace(final ReplaceRequest request, boolean ignoreNoChanges)
- throws IOException, OrmException {
- final RevCommit c = request.newCommit;
- rp.getRevWalk().parseBody(c);
- warnMalformedMessage(c);
+ for (ReplaceRequest req : replaceByChange.values()) {
+ if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) {
+ batch.addCommand(req.cmd);
+ }
+ }
- final Account.Id me = currentUser.getAccountId();
- final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
- final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
- final List<FooterLine> footerLines = c.getFooterLines();
- for (final FooterLine footerLine : footerLines) {
- try {
- if (isReviewer(footerLine)) {
- reviewers.add(toAccountId(footerLine.getValue().trim()));
- } else if (footerLine.matches(FooterKey.CC)) {
- cc.add(toAccountId(footerLine.getValue().trim()));
+ if (newChange != null && newChange.getResult() != NOT_ATTEMPTED) {
+ // Cancel creations tied to refs/for/ or refs/drafts/ command.
+ for (ReplaceRequest req : replaceByChange.values()) {
+ if (req.inputCommand == newChange && req.cmd != null) {
+ req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
}
- } catch (NoSuchAccountException e) {
- continue;
+ }
+ for (CreateRequest req : newChanges) {
+ req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
}
}
- reviewers.remove(me);
- cc.remove(me);
- cc.removeAll(reviewers);
-
- final ReplaceResult result = new ReplaceResult();
- final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
- final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+ }
- Change change = db.changes().get(request.ontoChange);
- if (change == null) {
- reject(request.cmd, "change " + request.ontoChange + " not found");
- return null;
+ private void readChangesForReplace() throws OrmException {
+ List<CheckedFuture<Change, OrmException>> futures =
+ Lists.newArrayListWithCapacity(replaceByChange.size());
+ for (ReplaceRequest request : replaceByChange.values()) {
+ futures.add(db.changes().getAsync(request.ontoChange));
}
- if (change.getStatus().isClosed()) {
- reject(request.cmd, "change " + request.ontoChange + " closed");
- return null;
+ for (CheckedFuture<Change, OrmException> f : futures) {
+ Change c = f.checkedGet();
+ if (c != null) {
+ replaceByChange.get(c.getId()).change = c;
+ }
}
+ }
- final ChangeControl changeCtl = projectControl.controlFor(change);
- if (!changeCtl.canAddPatchSet()) {
- reject(request.cmd, "cannot replace " + request.ontoChange);
- return null;
+ 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));
}
- if (!validCommit(changeCtl.getRefControl(), request.cmd, c)) {
- return null;
+ for (ReplaceRequest req : replaceByChange.values()) {
+ req.patchSets = results.get(req.ontoChange).toList();
+ }
+ }
+
+ private class ReplaceRequest {
+ final Change.Id ontoChange;
+ final RevCommit newCommit;
+ final ReceiveCommand inputCommand;
+ final boolean checkMergedInto;
+ Change change;
+ ChangeControl changeCtl;
+ List<PatchSet> patchSets;
+ PatchSet newPatchSet;
+ ReceiveCommand cmd;
+ PatchSetInfo info;
+ ChangeMessage msg;
+ String mergedIntoRef;
+ private PatchSet.Id priorPatchSet;
+
+ ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
+ final ReceiveCommand cmd, final boolean checkMergedInto) {
+ this.ontoChange = toChange;
+ this.newCommit = newCommit;
+ this.inputCommand = cmd;
+ this.checkMergedInto = checkMergedInto;
}
- final PatchSet.Id priorPatchSet = change.currentPatchSetId();
- for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) {
- if (ps.getRevision() == null) {
- log.warn("Patch set " + ps.getId() + " has no revision");
- reject(request.cmd, "change state corrupt");
- return null;
+ boolean validate(boolean autoClose) throws IOException {
+ if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
+ return false;
}
- 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(request.cmd, "change state corrupt");
- return null;
+ if (change == null || patchSets == null) {
+ reject(inputCommand, "change " + ontoChange + " not found");
+ return false;
}
- try {
- final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
+ if (change.getStatus().isClosed()) {
+ reject(inputCommand, "change " + ontoChange + " closed");
+ return false;
+ }
- // 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, c)) {
- reject(request.cmd, "squash commits first");
- return null;
+ changeCtl = projectControl.controlFor(change);
+ if (!changeCtl.canAddPatchSet()) {
+ reject(inputCommand, "cannot replace " + ontoChange);
+ 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 commit to appear twice on the same change
- //
- if (c == prior) {
- reject(request.cmd, "commit already exists");
- return null;
+ 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");
+ 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 (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) {
- rp.getRevWalk().parseBody(prior);
- final boolean messageEq =
- eq(c.getFullMessage(), prior.getFullMessage());
- final boolean parentsEq = parentsEqual(c, prior);
- final boolean authorEq = authorEqual(c, prior);
-
- if (messageEq && parentsEq && authorEq && !ignoreNoChanges) {
- reject(request.cmd, "no changes made");
- return null;
- } else {
- ObjectReader reader = rp.getRevWalk().getObjectReader();
- StringBuilder msg = new StringBuilder();
- msg.append("(W) ");
- msg.append(reader.abbreviate(c).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());
+ try {
+ final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
+
+ // 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;
}
- }
- } catch (IOException e) {
- log.error("Change " + change.getId() + " missing " + revIdStr, e);
- reject(request.cmd, "change state corrupt");
- return null;
- }
- }
- final PatchSet ps;
- final ChangeMessage msg;
- db.changes().beginTransaction(change.getId());
- try {
- change =
- db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isOpen()) {
- change.nextPatchSetId();
- change.setLastSha1MergeTested(null);
- return change;
+ // Don't allow the same commit to appear twice on the same change
+ //
+ if (newCommit == prior) {
+ reject(inputCommand, "commit already exists");
+ 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 (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 {
- return null;
+ 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 (change == null) {
- reject(request.cmd, "change is closed");
- return null;
+ } catch (IOException e) {
+ log.error("Change " + change.getId() + " missing " + revIdStr, e);
+ reject(inputCommand, "change state corrupt");
+ return false;
+ }
}
- ps = new PatchSet(change.currPatchSetId());
- ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
- ps.setUploader(currentUser.getAccountId());
- ps.setRevision(toRevId(c));
- if (MagicBranch.isDraft(request.cmd.getRefName())) {
- ps.setDraft(true);
+ change.nextPatchSetId();
+ newPatchSet = new PatchSet(change.currPatchSetId());
+ newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+ newPatchSet.setUploader(currentUser.getAccountId());
+ newPatchSet.setRevision(toRevId(newCommit));
+ if (newChange != null && MagicBranch.isDraft(newChange.getRefName())) {
+ newPatchSet.setDraft(true);
}
- insertAncestors(ps.getId(), c);
- db.patchSets().insert(Collections.singleton(ps));
+ info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+ cmd = new ReceiveCommand(
+ ObjectId.zeroId(),
+ newCommit,
+ newPatchSet.getRefName());
+ return true;
+ }
+
+ PatchSet.Id insertPatchSet() throws IOException, OrmException {
+ rp.getRevWalk().parseBody(newCommit);
+ warnMalformedMessage(newCommit);
- if (request.checkMergedInto) {
- final Ref mergedInto = findMergedInto(change.getDest().get(), c);
- result.mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
+ final Account.Id me = currentUser.getAccountId();
+ final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
+ final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
+ final List<FooterLine> footerLines = newCommit.getFooterLines();
+ for (final FooterLine footerLine : footerLines) {
+ try {
+ if (isReviewer(footerLine)) {
+ reviewers.add(toAccountId(footerLine.getValue().trim()));
+ } else if (footerLine.matches(FooterKey.CC)) {
+ cc.add(toAccountId(footerLine.getValue().trim()));
+ }
+ } catch (NoSuchAccountException e) {
+ continue;
+ }
}
- final PatchSetInfo info = patchSetInfoFactory.get(c, ps.getId());
- change.setCurrentPatchSet(info);
- result.change = change;
- result.patchSet = ps;
- result.info = info;
+ reviewers.remove(me);
+ cc.remove(me);
+ cc.removeAll(reviewers);
- final Account.Id authorId =
- result.info.getAuthor() != null ? result.info.getAuthor().getAccount()
- : null;
- final Account.Id committerId =
- result.info.getCommitter() != null ? result.info.getCommitter()
- .getAccount() : null;
+ final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
+ final Set<Account.Id> oldCC = new HashSet<Account.Id>();
- boolean haveAuthor = false;
- boolean haveCommitter = false;
- final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
+ db.changes().beginTransaction(change.getId());
+ try {
+ change =
+ db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isClosed()) {
+ return null;
+ }
- oldReviewers.clear();
- oldCC.clear();
+ change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
+ return change;
+ }
+ });
+ if (change == null) {
+ reject(inputCommand, "change is closed");
+ return null;
+ }
- for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
- haveApprovals.add(a.getAccountId());
+ insertAncestors(newPatchSet.getId(), newCommit);
+ db.patchSets().insert(Collections.singleton(newPatchSet));
- if (a.getValue() != 0) {
- oldReviewers.add(a.getAccountId());
- } else {
- oldCC.add(a.getAccountId());
+ if (checkMergedInto) {
+ final Ref mergedInto = findMergedInto(change.getDest().get(), newCommit);
+ mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
}
- // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
- if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
- final ApprovalType type =
- approvalTypes.byId(a.getCategoryId());
- if (a.getPatchSetId().equals(priorPatchSet)
- && type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
- // If there was a negative vote on the prior patch set, carry it
- // into this patch set.
- //
- db.patchSetApprovals().insert(
- Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+ List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
+
+ final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
+ oldReviewers.clear();
+ oldCC.clear();
+
+ for (PatchSetApproval a : patchSetApprovals) {
+ haveApprovals.add(a.getAccountId());
+ if (a.getValue() != 0) {
+ oldReviewers.add(a.getAccountId());
+ } else {
+ oldCC.add(a.getAccountId());
}
}
- if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
- haveAuthor = true;
- }
- if (!haveCommitter && committerId != null
- && a.getAccountId().equals(committerId)) {
- haveCommitter = true;
- }
- }
+ approvalsUtil.addReviewers(change, newPatchSet, info, reviewers, haveApprovals);
- final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
- if (allTypes.size() > 0) {
- final ApprovalCategory.Id catId =
- allTypes.get(allTypes.size() - 1).getCategory().getId();
- if (authorId != null && haveApprovals.add(authorId)) {
- insertDummyApproval(result, authorId, catId, db);
- }
- if (committerId != null && haveApprovals.add(committerId)) {
- insertDummyApproval(result, committerId, catId, db);
+ msg =
+ new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
+ .messageUUID(db)), me, newPatchSet.getCreatedOn(), newPatchSet.getId());
+ msg.setMessage("Uploaded patch set " + newPatchSet.getPatchSetId() + ".");
+ db.changeMessages().insert(Collections.singleton(msg));
+ if (change.currentPatchSetId().equals(priorPatchSet)) {
+ ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
}
- for (final Account.Id reviewer : reviewers) {
- if (haveApprovals.add(reviewer)) {
- insertDummyApproval(result, reviewer, catId, db);
- }
- }
- }
- msg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
- .messageUUID(db)), me, ps.getCreatedOn(), ps.getId());
- msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
- db.changeMessages().insert(Collections.singleton(msg));
- ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
- result.msg = msg;
+ if (mergedIntoRef == null) {
+ // Change should be new, so it can go through review again.
+ //
+ change =
+ db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+ @Override
+ public Change update(Change change) {
+ if (change.getStatus().isClosed()) {
+ return null;
+ }
+
+ if (!change.currentPatchSetId().equals(priorPatchSet)) {
+ return change;
+ }
- if (result.mergedIntoRef == null) {
- // Change should be new, so it can go through review again.
- //
- change =
- db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
- @Override
- public Change update(Change change) {
- if (change.getStatus().isOpen()) {
if (destTopicName != null) {
change.setTopic(destTopicName);
}
- if (change.getStatus() == Change.Status.DRAFT && ps.isDraft()) {
+ if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) {
// Leave in draft status.
} else {
change.setStatus(Change.Status.NEW);
}
- change.setCurrentPatchSet(result.info);
+ change.setLastSha1MergeTested(null);
+ change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
return change;
- } else {
- return null;
}
- }
- });
- if (change == null) {
- db.patchSets().delete(Collections.singleton(ps));
- db.changeMessages().delete(Collections.singleton(msg));
- reject(request.cmd, "change is closed");
- return null;
+ });
+ if (change == null) {
+ db.patchSets().delete(Collections.singleton(newPatchSet));
+ db.changeMessages().delete(Collections.singleton(msg));
+ reject(inputCommand, "change is closed");
+ return null;
+ }
}
- }
- db.commit();
- } finally {
- db.rollback();
- }
-
- if (result.mergedIntoRef != null) {
- // Change was already submitted to a branch, close it.
- //
- markChangeMergedByPush(db, result);
- }
-
- final RefUpdate ru = repo.updateRef(ps.getRefName());
- ru.setNewObjectId(c);
- ru.disableRefLog();
- if (ru.update(rp.getRevWalk()) != RefUpdate.Result.NEW) {
- throw new IOException("Failed to create ref " + ps.getRefName() + " in "
- + repo.getDirectory() + ": " + ru.getResult());
- }
- replication.scheduleUpdate(project.getNameKey(), ru.getName());
- hooks.doPatchsetCreatedHook(result.change, ps, db);
- request.cmd.setResult(OK);
+ db.commit();
+ } finally {
+ db.rollback();
+ }
- workQueue.getDefaultQueue()
- .submit(requestScopePropagator.wrap(new Runnable() {
- @Override
- public void run() {
- try {
- final ReplacePatchSetSender cm;
- cm = replacePatchSetFactory.create(result.change);
- cm.setFrom(me);
- cm.setPatchSet(ps, result.info);
- cm.setChangeMessage(result.msg);
- cm.addReviewers(reviewers);
- cm.addExtraCC(cc);
- cm.addReviewers(oldReviewers);
- cm.addExtraCC(oldCC);
- cm.send();
- } catch (Exception e) {
- log.error("Cannot send email for new patch set " + ps.getId(), e);
- }
+ if (mergedIntoRef != null) {
+ // Change was already submitted to a branch, close it.
+ //
+ markChangeMergedByPush(db, this);
}
- @Override
- public String toString() {
- return "send-email newpatchset";
+ if (cmd.getResult() == NOT_ATTEMPTED) {
+ cmd.execute(rp);
}
- }));
+ replication.fire(project.getNameKey(), newPatchSet.getRefName());
+ hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+ replaceProgress.update(1);
+ if (mergedIntoRef != null) {
+ hooks.doChangeMergedHook(
+ change, currentUser.getAccount(), newPatchSet, db);
+ }
+ workQueue.getDefaultQueue()
+ .submit(requestScopePropagator.wrap(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ReplacePatchSetSender cm =
+ replacePatchSetFactory.create(change);
+ cm.setFrom(me);
+ cm.setPatchSet(newPatchSet, info);
+ cm.setChangeMessage(msg);
+ cm.addReviewers(reviewers);
+ cm.addExtraCC(cc);
+ cm.addReviewers(oldReviewers);
+ cm.addExtraCC(oldCC);
+ cm.send();
+ } catch (Exception e) {
+ log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+ }
+ if (mergedIntoRef != null) {
+ sendMergedEmail(ReplaceRequest.this);
+ }
+ }
- sendMergedEmail(result);
- return result != null ? result.info.getKey() : null;
+ @Override
+ public String toString() {
+ return "send-email newpatchset";
+ }
+ }));
+ return newPatchSet.getId();
+ }
}
static boolean parentsEqual(RevCommit a, RevCommit b) {
@@ -1534,23 +1771,6 @@ public class ReceiveCommits {
}
}
- private void insertDummyApproval(final ReplaceResult result,
- final Account.Id forAccount, final ApprovalCategory.Id catId,
- final ReviewDb db) throws OrmException {
- insertDummyApproval(result.change, result.patchSet.getId(), forAccount,
- catId, db);
- }
-
- private void insertDummyApproval(final Change change, final PatchSet.Id psId,
- final Account.Id forAccount, final ApprovalCategory.Id catId,
- final ReviewDb db) throws OrmException {
- final PatchSetApproval ca =
- new PatchSetApproval(new PatchSetApproval.Key(psId, forAccount, catId),
- (short) 0);
- ca.cache(change);
- db.patchSetApprovals().insert(Collections.singleton(ca));
- }
-
private Ref findMergedInto(final String first, final RevCommit commit) {
try {
final Map<String, Ref> all = repo.getAllRefs();
@@ -1578,46 +1798,32 @@ public class ReceiveCommits {
return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId()));
}
- private static class ReplaceRequest {
- final Change.Id ontoChange;
- final RevCommit newCommit;
- final ReceiveCommand cmd;
- final boolean checkMergedInto;
-
- ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
- final ReceiveCommand cmd, final boolean checkMergedInto) {
- this.ontoChange = toChange;
- this.newCommit = newCommit;
- this.cmd = cmd;
- this.checkMergedInto = checkMergedInto;
+ private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
+ if (ctl.canForgeAuthor()
+ && ctl.canForgeCommitter()
+ && ctl.canForgeGerritServerIdentity()
+ && ctl.canUploadMerges()
+ && !project.isUseSignedOffBy()
+ && Iterables.isEmpty(rejectCommits)
+ && !GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())
+ && !(MagicBranch.isMagicBranch(cmd.getRefName())
+ || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
+ return;
}
- }
- private static class ReplaceResult {
- Change change;
- PatchSet patchSet;
- PatchSetInfo info;
- ChangeMessage msg;
- String mergedIntoRef;
- }
-
- private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
final RevWalk walk = rp.getRevWalk();
walk.reset();
walk.sort(RevSort.NONE);
try {
+ Set<ObjectId> existing = Sets.newHashSet();
walk.markStart(walk.parseCommit(cmd.getNewId()));
- for (ObjectId id : existingObjects()) {
- try {
- walk.markUninteresting(walk.parseCommit(id));
- } catch (IOException e) {
- continue;
- }
- }
+ markHeadsAsUninteresting(walk, existing);
RevCommit c;
while ((c = walk.next()) != null) {
- if (!validCommit(ctl, cmd, c)) {
+ if (existing.contains(c)) {
+ continue;
+ } else if (!validCommit(ctl, cmd, c)) {
break;
}
}
@@ -1627,17 +1833,6 @@ public class ReceiveCommits {
}
}
- private Collection<ObjectId> existingObjects() {
- if (existingObjects == null) {
- Map<String, Ref> refs = repo.getAllRefs();
- existingObjects = new ArrayList<ObjectId>(refs.size());
- for (Ref r : refs.values()) {
- existingObjects.add(r.getObjectId());
- }
- }
- return existingObjects;
- }
-
private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
final RevCommit c) throws MissingObjectException, IOException {
rp.getRevWalk().parseBody(c);
@@ -1702,7 +1897,7 @@ public class ReceiveCommits {
}
final List<String> idList = c.getFooterLines(CHANGE_ID);
- if ((MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
+ if (MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches()) {
if (idList.isEmpty()) {
if (project.isRequireChangeID()) {
String errMsg = "missing Change-Id in commit message";
@@ -1874,24 +2069,31 @@ public class ReceiveCommits {
final Ref ref = byCommit.get(c.copy());
if (ref != null) {
rw.parseBody(c);
- closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
+ Change.Key closedChange =
+ closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
closeProgress.update(1);
+ if (closedChange != null) {
+ byKey.remove(closedChange);
+ }
}
rw.parseBody(c);
for (final String changeId : c.getFooterLines(CHANGE_ID)) {
final Change.Id onto = byKey.get(new Change.Key(changeId.trim()));
if (onto != null) {
- toClose.add(new ReplaceRequest(onto, c, cmd, false));
+ 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;
}
}
}
for (final ReplaceRequest req : toClose) {
- final PatchSet.Id psi = doReplace(req, true);
+ final PatchSet.Id psi = req.validate(true) ? req.insertPatchSet() : null;
if (psi != null) {
- closeChange(req.cmd, psi, req.newCommit);
+ closeChange(req.inputCommand, psi, req.newCommit);
closeProgress.update(1);
}
}
@@ -1916,7 +2118,7 @@ public class ReceiveCommits {
}
}
- private void closeChange(final ReceiveCommand cmd, final PatchSet.Id psi,
+ private Change.Key closeChange(final ReceiveCommand cmd, final PatchSet.Id psi,
final RevCommit commit) throws OrmException {
final String refName = cmd.getRefName();
final Change.Id cid = psi.getParentKey();
@@ -1925,7 +2127,7 @@ public class ReceiveCommits {
final PatchSet ps = db.patchSets().get(psi);
if (change == null || ps == null) {
log.warn(project.getName() + " " + psi + " is missing");
- return;
+ return null;
}
if (change.getStatus() == Change.Status.MERGED ||
@@ -1934,17 +2136,19 @@ public class ReceiveCommits {
// might just be moving from an experimental branch into
// a more stable branch.
//
- return;
+ return null;
}
- final ReplaceResult result = new ReplaceResult();
+ ReplaceRequest result = new ReplaceRequest(cid, commit, cmd, false);
result.change = change;
- result.patchSet = ps;
+ result.newPatchSet = ps;
result.info = patchSetInfoFactory.get(commit, psi);
result.mergedIntoRef = refName;
-
markChangeMergedByPush(db, result);
+ hooks.doChangeMergedHook(
+ change, currentUser.getAccount(), result.newPatchSet, db);
sendMergedEmail(result);
+ return change.getKey();
}
private Map<ObjectId, Ref> changeRefsById() throws IOException {
@@ -1969,7 +2173,7 @@ public class ReceiveCommits {
}
private void markChangeMergedByPush(final ReviewDb db,
- final ReplaceResult result) throws OrmException {
+ final ReplaceRequest result) throws OrmException {
final Change change = result.change;
final String mergedIntoRef = result.mergedIntoRef;
@@ -1977,7 +2181,7 @@ public class ReceiveCommits {
change.setStatus(Change.Status.MERGED);
ChangeUtil.updated(change);
- ApprovalsUtil.syncChangeStatus(db, change);
+ approvalsUtil.syncChangeStatus(change);
final StringBuilder msgBuf = new StringBuilder();
msgBuf.append("Change has been successfully pushed");
@@ -2011,36 +2215,27 @@ public class ReceiveCommits {
});
}
- private void sendMergedEmail(final ReplaceResult result) {
- if (result != null && result.mergedIntoRef != null) {
- workQueue.getDefaultQueue()
- .submit(requestScopePropagator.wrap(new Runnable() {
- @Override
- public void run() {
- try {
- final MergedSender cm = mergedSenderFactory.create(result.change);
- cm.setFrom(currentUser.getAccountId());
- cm.setPatchSet(result.patchSet, result.info);
- cm.send();
- } catch (Exception e) {
- final PatchSet.Id psi = result.patchSet.getId();
- log.error("Cannot send email for submitted patch set " + psi, e);
- }
- }
-
- @Override
- public String toString() {
- return "send-email merged";
+ private void sendMergedEmail(final ReplaceRequest result) {
+ workQueue.getDefaultQueue()
+ .submit(requestScopePropagator.wrap(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final MergedSender cm = mergedSenderFactory.create(result.change);
+ cm.setFrom(currentUser.getAccountId());
+ cm.setPatchSet(result.newPatchSet, result.info);
+ cm.send();
+ } catch (Exception e) {
+ final PatchSet.Id psi = result.newPatchSet.getId();
+ log.error("Cannot send email for submitted patch set " + psi, e);
}
- }));
+ }
- try {
- hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
- result.patchSet, db);
- } catch (OrmException err) {
- log.error("Cannot open change: " + result.change.getChangeId(), err);
+ @Override
+ public String toString() {
+ return "send-email merged";
}
- }
+ }));
}
private void insertAncestors(PatchSet.Id id, RevCommit src)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index e87fe2be83..ec8c080bc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,7 +16,7 @@ package com.google.gerrit.server.git;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.BaseReceivePack;
import org.eclipse.jgit.transport.UploadPack;
import java.util.HashMap;
@@ -31,7 +31,7 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
}
@Override
- public void advertiseRefs(ReceivePack rp) {
+ public void advertiseRefs(BaseReceivePack rp) {
Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
if (oldRefs == null) {
oldRefs = rp.getRepository().getAllRefs();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index f5e8fa8fb0..74900065d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -123,11 +123,13 @@ public class RenameGroupOp extends DefaultQueueOp {
ref.setName(newName);
md.getCommitBuilder().setAuthor(author);
md.setMessage("Rename group " + oldName + " to " + newName + "\n");
- if (config.commit(md)) {
+ try {
+ config.commit(md);
projectCache.evict(config.getProject());
success = true;
-
- } else {
+ } catch (IOException e) {
+ log.error("Could not commit rename of group " + oldName + " to "
+ + newName + " in " + md.getProjectName().get(), e);
try {
Thread.sleep(25 /* milliseconds */);
} catch (InterruptedException wakeUp) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
deleted file mode 100644
index f7bb9f8f11..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// 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.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** Manages replication to other nodes. */
-public interface ReplicationQueue {
- /** Is replication to one or more other destinations configured? */
- boolean isEnabled();
-
- /**
- * Schedule a full replication for a single project.
- * <p>
- * All remote URLs are checked to verify the are current with regards to the
- * local project state. If not, they are updated by pushing new refs, updating
- * existing ones which don't match, and deleting stale refs which have been
- * removed from the local repository.
- *
- * @param project identity of the project to replicate.
- * @param urlMatch substring that must appear in a URI to support replication.
- */
- void scheduleFullSync(Project.NameKey project, String urlMatch);
-
- /**
- * Schedule update of a single ref.
- * <p>
- * This method automatically tries to batch together multiple requests in the
- * same project, to take advantage of Git's native ability to update multiple
- * refs during a single push operation.
- *
- * @param project identity of the project to replicate.
- * @param ref unique name of the ref; must start with {@code refs/}.
- */
- void scheduleUpdate(Project.NameKey project, String ref);
-
- /**
- * Create new empty project at the remote sites.
- * <p>
- * When a new project has been created locally call this method to make sure
- * that the project will be created at the remote sites as well.
- *
- * @param project of the project to be created.
- * @param head name HEAD should point at (must be {@code refs/heads/...}).
- */
- void replicateNewProject(Project.NameKey project, String head);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
index 64ba6e8df4..98ddf80434 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.git;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
deleted file mode 100644
index d51936ac89..0000000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2011 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.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.CredentialItem;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.URIish;
-
-/** Looks up a remote's password in secure.config. */
-public class SecureCredentialsProvider extends CredentialsProvider {
- public interface Factory {
- SecureCredentialsProvider create(String remoteName);
- }
-
- private final String cfgUser;
- private final String cfgPass;
-
- @Inject
- SecureCredentialsProvider(@GerritServerConfig Config cfg,
- @Assisted String remoteName) {
- cfgUser = cfg.getString("remote", remoteName, "username");
- cfgPass = cfg.getString("remote", remoteName, "password");
- }
-
- @Override
- public boolean isInteractive() {
- return false;
- }
-
- @Override
- public boolean supports(CredentialItem... items) {
- for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username) {
- continue;
- } else if (i instanceof CredentialItem.Password) {
- continue;
- } else {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public boolean get(URIish uri, CredentialItem... items)
- throws UnsupportedCredentialItem {
- String username = uri.getUser();
- if (username == null) {
- username = cfgUser;
- }
- if (username == null) {
- return false;
- }
-
- String password = uri.getPass();
- if (password == null) {
- password = cfgPass;
- }
- if (password == null) {
- return false;
- }
-
- for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username) {
- ((CredentialItem.Username) i).setValue(username);
- } else if (i instanceof CredentialItem.Password) {
- ((CredentialItem.Password) i).setValue(password.toCharArray());
- } else {
- throw new UnsupportedCredentialItem(uri, i.getPromptText());
- }
- }
- return true;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 706ba7d816..ccb91a3a9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -21,6 +21,7 @@ import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.util.SubmoduleSectionParser;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -84,7 +85,7 @@ public class SubmoduleOp {
private final Map<Change.Id, CodeReviewCommit> commits;
private final PersonIdent myIdent;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final SchemaFactory<ReviewDb> schemaFactory;
private final Set<Branch.NameKey> updatedSubscribers;
@@ -96,7 +97,7 @@ public class SubmoduleOp {
@Assisted Project destProject, @Assisted List<Change> submitted,
@Assisted final Map<Change.Id, CodeReviewCommit> commits,
@GerritPersonIdent final PersonIdent myIdent,
- GitRepositoryManager repoManager, ReplicationQueue replication) {
+ GitRepositoryManager repoManager, GitReferenceUpdated replication) {
this.destBranch = destBranch;
this.mergeTip = mergeTip;
this.rw = rw;
@@ -118,7 +119,7 @@ public class SubmoduleOp {
schema = schemaFactory.open();
updateSubmoduleSubscriptions();
- updateSuperProjects(destBranch, mergeTip.getId().toObjectId(), null);
+ updateSuperProjects(destBranch, rw, mergeTip.getId().toObjectId(), null);
} catch (OrmException e) {
throw new SubmoduleException("Cannot open database", e);
} finally {
@@ -192,7 +193,7 @@ public class SubmoduleOp {
}
}
- private void updateSuperProjects(final Branch.NameKey updatedBranch,
+ private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
final ObjectId mergedCommit, final String msg) throws SubmoduleException {
try {
final List<SubmoduleSubscription> subscribers =
@@ -201,6 +202,9 @@ public class SubmoduleOp {
if (!subscribers.isEmpty()) {
String msgbuf = msg;
if (msgbuf == null) {
+ // Initialize the message buffer
+ msgbuf = "";
+
// The first updatedBranch on a cascade event of automatic
// updates of repos is added to updatedSubscribers set so
// if we face a situation having
@@ -236,7 +240,7 @@ public class SubmoduleOp {
paths.put(updatedBranch, s.getPath());
try {
- updateGitlinks(s.getSuperProject(), modules, paths, msgbuf);
+ updateGitlinks(s.getSuperProject(), myRw, modules, paths, msgbuf);
} catch (SubmoduleException e) {
throw e;
}
@@ -248,7 +252,7 @@ public class SubmoduleOp {
}
}
- private void updateGitlinks(final Branch.NameKey subscriber,
+ private void updateGitlinks(final Branch.NameKey subscriber, RevWalk myRw,
final Map<Branch.NameKey, ObjectId> modules,
final Map<Branch.NameKey, String> paths, final String msg)
throws SubmoduleException {
@@ -257,12 +261,13 @@ public class SubmoduleOp {
final StringBuilder msgbuf = new StringBuilder();
msgbuf.append("Updated " + subscriber.getParentKey().get());
Repository pdb = null;
+ RevWalk recRw = null;
try {
boolean sameAuthorForAll = true;
for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
- RevCommit c = rw.parseCommit(me.getValue());
+ RevCommit c = myRw.parseCommit(me.getValue());
msgbuf.append("\nProject: ");
msgbuf.append(me.getKey().getParentKey().get());
@@ -331,7 +336,7 @@ public class SubmoduleOp {
switch (rfu.update()) {
case NEW:
case FAST_FORWARD:
- replication.scheduleUpdate(subscriber.getParentKey(), rfu.getName());
+ replication.fire(subscriber.getParentKey(), rfu.getName());
// TODO since this is performed "in the background" no mail will be
// sent to inform users about the updated branch
break;
@@ -340,12 +345,17 @@ public class SubmoduleOp {
throw new IOException(rfu.getResult().name());
}
+ recRw = new RevWalk(pdb);
+
// Recursive call: update subscribers of the subscriber
- updateSuperProjects(subscriber, commitId, msgbuf.toString());
+ updateSuperProjects(subscriber, recRw, commitId, msgbuf.toString());
} catch (IOException e) {
logAndThrowSubmoduleException("Cannot update gitlinks for "
+ subscriber.get(), e);
} finally {
+ if (recRw != null) {
+ recRw.release();
+ }
if (pdb != null) {
pdb.close();
}
@@ -355,14 +365,17 @@ public class SubmoduleOp {
private static DirCache readTree(final Repository pdb, final Ref branch)
throws MissingObjectException, IncorrectObjectTypeException, IOException {
final RevWalk rw = new RevWalk(pdb);
-
- final DirCache dc = DirCache.newInCore();
- final DirCacheBuilder b = dc.builder();
- b.addTree(new byte[0], // no prefix path
- DirCacheEntry.STAGE_0, // standard stage
- pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
- b.finish();
- return dc;
+ try {
+ final DirCache dc = DirCache.newInCore();
+ final DirCacheBuilder b = dc.builder();
+ b.addTree(new byte[0], // no prefix path
+ DirCacheEntry.STAGE_0, // standard stage
+ pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
+ b.finish();
+ return dc;
+ } finally {
+ rw.release();
+ }
}
private static void logAndThrowSubmoduleException(final String errorMsg,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index ac4882fa0b..3c64229d3d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -14,13 +14,12 @@
package com.google.gerrit.server.git;
+import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.lib.ObjectId;
@@ -38,19 +37,17 @@ public class TagCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<EntryKey, EntryVal>> type =
- new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
- disk(type, CACHE_NAME);
+ persist(CACHE_NAME, String.class, EntryVal.class);
bind(TagCache.class);
}
};
}
- private final Cache<EntryKey, EntryVal> cache;
+ private final Cache<String, EntryVal> cache;
private final Object createLock = new Object();
@Inject
- TagCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+ TagCache(@Named(CACHE_NAME) Cache<String, EntryVal> cache) {
this.cache = cache;
}
@@ -74,67 +71,43 @@ public class TagCache {
// never fail with an exception. Some of these references can be null
// (e.g. not all projects are cached, or the cache is not current).
//
- EntryVal val = cache.get(new EntryKey(name));
+ EntryVal val = cache.getIfPresent(name.get());
if (val != null) {
TagSetHolder holder = val.holder;
if (holder != null) {
TagSet tags = holder.getTagSet();
if (tags != null) {
- tags.updateFastForward(refName, oldValue, newValue);
+ if (tags.updateFastForward(refName, oldValue, newValue)) {
+ cache.put(name.get(), val);
+ }
}
}
}
}
TagSetHolder get(Project.NameKey name) {
- EntryKey key = new EntryKey(name);
- EntryVal val = cache.get(key);
+ EntryVal val = cache.getIfPresent(name.get());
if (val == null) {
synchronized (createLock) {
- val = cache.get(key);
+ val = cache.getIfPresent(name.get());
if (val == null) {
val = new EntryVal();
val.holder = new TagSetHolder(name);
- cache.put(key, val);
+ cache.put(name.get(), val);
}
}
}
return val.holder;
}
- static class EntryKey implements Serializable {
- static final long serialVersionUID = 1L;
-
- private transient String name;
-
- EntryKey(Project.NameKey name) {
- this.name = name.get();
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof EntryKey) {
- return name.equals(((EntryKey) o).name);
- }
- return false;
- }
-
- private void readObject(ObjectInputStream in) throws IOException {
- name = in.readUTF();
- }
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- out.writeUTF(name);
- }
+ void put(Project.NameKey name, TagSetHolder tags) {
+ EntryVal val = new EntryVal();
+ val.holder = tags;
+ cache.put(name.get(), val);
}
static class EntryVal implements Serializable {
- static final long serialVersionUID = EntryKey.serialVersionUID;
+ static final long serialVersionUID = 1L;
transient TagSetHolder holder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index 6cf873d1c2..7d95db20a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -30,15 +30,22 @@ class TagMatcher {
final List<Ref> newRefs = new ArrayList<Ref>();
final List<LostRef> lostRefs = new ArrayList<LostRef>();
final TagSetHolder holder;
+ final TagCache cache;
final Repository db;
final Collection<Ref> include;
TagSet tags;
- boolean updated;
+ final boolean updated;
private boolean rebuiltForNewTags;
- TagMatcher(TagSetHolder holder, Repository db, Collection<Ref> include,
- TagSet tags, boolean updated) {
+ TagMatcher(
+ TagSetHolder holder,
+ TagCache cache,
+ Repository db,
+ Collection<Ref> include,
+ TagSet tags,
+ boolean updated) {
this.holder = holder;
+ this.cache = cache;
this.db = db;
this.include = include;
this.tags = tags;
@@ -63,7 +70,7 @@ class TagMatcher {
}
rebuiltForNewTags = true;
- holder.rebuildForNewTags(this);
+ holder.rebuildForNewTags(cache, this);
return isReachable(tagRef);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index 8830580742..c57942cf1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -58,7 +58,7 @@ class TagSet {
return tags.get(id);
}
- void updateFastForward(String refName, ObjectId oldValue,
+ boolean updateFastForward(String refName, ObjectId oldValue,
ObjectId newValue) {
CachedRef ref = refs.get(refName);
if (ref != null) {
@@ -68,9 +68,10 @@ class TagSet {
//
ObjectId cur = ref.get();
if (cur.equals(oldValue)) {
- ref.compareAndSet(cur, newValue);
+ return ref.compareAndSet(cur, newValue);
}
}
+ return false;
}
void prepare(TagMatcher m) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index 91c8a5c161..d5120e056e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -42,51 +42,52 @@ class TagSetHolder {
this.tags = tags;
}
- TagMatcher matcher(Repository db, Collection<Ref> include) {
+ TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
TagSet tags = this.tags;
if (tags == null) {
- tags = build(db);
+ tags = build(cache, db);
}
- TagMatcher m = new TagMatcher(this, db, include, tags, false);
+ TagMatcher m = new TagMatcher(this, cache, db, include, tags, false);
tags.prepare(m);
if (!m.newRefs.isEmpty() || !m.lostRefs.isEmpty()) {
- tags = rebuild(db, tags, m);
+ tags = rebuild(cache, db, tags, m);
- m = new TagMatcher(this, db, include, tags, true);
+ m = new TagMatcher(this, cache, db, include, tags, true);
tags.prepare(m);
}
return m;
}
- void rebuildForNewTags(TagMatcher m) {
- m.tags = rebuild(m.db, m.tags, null);
-
+ void rebuildForNewTags(TagCache cache, TagMatcher m) {
+ m.tags = rebuild(cache, m.db, m.tags, null);
m.mask.clear();
m.newRefs.clear();
m.lostRefs.clear();
m.tags.prepare(m);
}
- private TagSet build(Repository db) {
+ private TagSet build(TagCache cache, Repository db) {
synchronized (buildLock) {
TagSet tags = this.tags;
if (tags == null) {
tags = new TagSet(projectName);
tags.build(db, null, null);
this.tags = tags;
+ cache.put(projectName, this);
}
return tags;
}
}
- private TagSet rebuild(Repository db, TagSet old, TagMatcher m) {
+ private TagSet rebuild(TagCache cache, Repository db, TagSet old, TagMatcher m) {
synchronized (buildLock) {
TagSet cur = this.tags;
if (cur == old) {
cur = new TagSet(projectName);
cur.build(db, old, m);
this.tags = cur;
+ cache.put(projectName, this);
}
return cur;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index c34cc5409c..e9c5536750 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.git;
+import com.google.common.base.Objects;
+
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -23,7 +25,7 @@ import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
@@ -144,78 +146,177 @@ public abstract class VersionedMetaData {
* Update this metadata branch, recording a new commit on its reference.
*
* @param update helper information to define the update that will occur.
- * @return true if the update was successful, false if it failed because of a
- * concurrent update to the same reference.
+ * @return the commit that was created
+ * @throws IOException if there is a storage problem and the update cannot be
+ * executed as requested or if it failed because of a concurrent
+ * update to the same reference
+ */
+ public RevCommit commit(MetaDataUpdate update) throws IOException {
+ BatchMetaDataUpdate batch = openUpdate(update);
+ try {
+ batch.write(update.getCommitBuilder());
+ return batch.commit();
+ } finally {
+ batch.close();
+ }
+ }
+
+ /**
+ * Creates a new commit and a new ref based on this commit.
+ *
+ * @param update helper information to define the update that will occur.
+ * @param refName name of the ref that should be created
+ * @return the commit that was created
* @throws IOException if there is a storage problem and the update cannot be
- * executed as requested.
+ * executed as requested or if it failed because of a concurrent
+ * update to the same reference
*/
- public boolean commit(MetaDataUpdate update) throws IOException {
+ public RevCommit commitToNewRef(MetaDataUpdate update, String refName) throws IOException {
+ BatchMetaDataUpdate batch = openUpdate(update);
+ try {
+ batch.write(update.getCommitBuilder());
+ return batch.createRef(refName);
+ } finally {
+ batch.close();
+ }
+ }
+
+ public interface BatchMetaDataUpdate {
+ void write(CommitBuilder commit) throws IOException;
+ void write(VersionedMetaData config, CommitBuilder commit) throws IOException;
+ RevCommit createRef(String refName) throws IOException;
+ RevCommit commit() throws IOException;
+ RevCommit commitAt(ObjectId revision) throws IOException;
+ void close();
+ }
+
+ public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException {
final Repository db = update.getRepository();
- final CommitBuilder commit = update.getCommitBuilder();
reader = db.newObjectReader();
inserter = db.newObjectInserter();
- try {
- final RevWalk rw = new RevWalk(reader);
- final RevTree src = revision != null ? rw.parseTree(revision) : null;
- final ObjectId res = writeTree(src, commit);
+ final RevWalk rw = new RevWalk(reader);
+ final RevTree tree = revision != null ? rw.parseTree(revision) : null;
+ newTree = readTree(tree);
+ return new BatchMetaDataUpdate() {
+ AnyObjectId src = revision;
+ AnyObjectId srcTree = tree;
+
+ @Override
+ public void write(CommitBuilder commit) throws IOException {
+ write(VersionedMetaData.this, commit);
+ }
- if (res.equals(src)) {
- // If there are no changes to the content, don't create the commit.
- return true;
+ private void doSave(VersionedMetaData config, CommitBuilder commit) throws IOException {
+ DirCache nt = config.newTree;
+ ObjectReader r = config.reader;
+ ObjectInserter i = config.inserter;
+ try {
+ config.newTree = newTree;
+ config.reader = reader;
+ config.inserter = inserter;
+ config.onSave(commit);
+ } catch (ConfigInvalidException e) {
+ throw new IOException("Cannot update " + getRefName() + " in "
+ + db.getDirectory() + ": " + e.getMessage(), e);
+ } finally {
+ config.newTree = nt;
+ config.reader = r;
+ config.inserter = i;
+ }
}
- commit.setTreeId(res);
- if (revision != null) {
- commit.setParentId(revision);
+ @Override
+ public void write(VersionedMetaData config, CommitBuilder commit) throws IOException {
+ doSave(config, commit);
+
+ final ObjectId res = newTree.writeTree(inserter);
+ if (res.equals(srcTree)) {
+ // If there are no changes to the content, don't create the commit.
+ return;
+ }
+
+ commit.setTreeId(res);
+ if (src != null) {
+ commit.addParentId(src);
+ }
+
+ src = inserter.insert(commit);
+ srcTree = res;
}
- RefUpdate ru = db.updateRef(getRefName());
- if (revision != null) {
- ru.setExpectedOldObjectId(revision);
- } else {
+ @Override
+ public RevCommit createRef(String refName) throws IOException {
+ if (Objects.equal(src, revision)) {
+ return revision;
+ }
+
+ RefUpdate ru = db.updateRef(refName);
ru.setExpectedOldObjectId(ObjectId.zeroId());
+ ru.setNewObjectId(src);
+ ru.disableRefLog();
+ inserter.flush();
+ RefUpdate.Result result = ru.update();
+ switch (result) {
+ case NEW:
+ revision = rw.parseCommit(ru.getNewObjectId());
+ update.replicate(ru.getName());
+ return revision;
+ default:
+ throw new IOException("Cannot update " + ru.getName() + " in "
+ + db.getDirectory() + ": " + ru.getResult());
+ }
}
- ru.setNewObjectId(inserter.insert(commit));
- ru.disableRefLog();
- inserter.flush();
-
- switch (ru.update(rw)) {
- case NEW:
- case FAST_FORWARD:
- revision = rw.parseCommit(ru.getNewObjectId());
- update.replicate(ru.getName());
- return true;
-
- case LOCK_FAILURE:
- return false;
-
- default:
- throw new IOException("Cannot update " + ru.getName() + " in "
- + db.getDirectory() + ": " + ru.getResult());
+
+ @Override
+ public RevCommit commit() throws IOException {
+ return commitAt(revision);
}
- } catch (ConfigInvalidException e) {
- throw new IOException("Cannot update " + getRefName() + " in "
- + db.getDirectory() + ": " + e.getMessage(), e);
- } finally {
- inserter.release();
- inserter = null;
- reader.release();
- reader = null;
- }
- }
+ @Override
+ public RevCommit commitAt(ObjectId expected) throws IOException {
+ if (Objects.equal(src, expected)) {
+ return revision;
+ }
- private ObjectId writeTree(RevTree srcTree, CommitBuilder commit)
- throws IOException, MissingObjectException, IncorrectObjectTypeException,
- UnmergedPathException, ConfigInvalidException {
- try {
- newTree = readTree(srcTree);
- onSave(commit);
- return newTree.writeTree(inserter);
- } finally {
- newTree = null;
- }
+ RefUpdate ru = db.updateRef(getRefName());
+ if (expected != null) {
+ ru.setExpectedOldObjectId(expected);
+ } else {
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ }
+ ru.setNewObjectId(src);
+ ru.disableRefLog();
+ inserter.flush();
+
+ switch (ru.update(rw)) {
+ case NEW:
+ case FAST_FORWARD:
+ revision = rw.parseCommit(ru.getNewObjectId());
+ update.replicate(ru.getName());
+ return revision;
+
+ default:
+ throw new IOException("Cannot update " + ru.getName() + " in "
+ + db.getDirectory() + ": " + ru.getResult());
+ }
+ }
+
+ @Override
+ public void close() {
+ newTree = null;
+
+ if (inserter != null) {
+ inserter.release();
+ inserter = null;
+ }
+
+ if (reader != null) {
+ reader.release();
+ reader = null;
+ }
+ }
+ };
}
private DirCache readTree(RevTree tree) throws IOException,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index fc47f10aa8..8d27c0e4cb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.git;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
@@ -60,12 +62,21 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
}
public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
+ if (projectCtl.allRefsAreVisibleExcept(
+ ImmutableSet.of(GitRepositoryManager.REF_CONFIG))) {
+ Map<String, Ref> r = Maps.newHashMap(refs);
+ r.remove(GitRepositoryManager.REF_CONFIG);
+ return r;
+ }
+
final Set<Change.Id> visibleChanges = visibleChanges();
final Map<String, Ref> result = new HashMap<String, Ref>();
final List<Ref> deferredTags = new ArrayList<Ref>();
for (Ref ref : refs.values()) {
- if (PatchSet.isRef(ref.getName())) {
+ if (ref.getName().startsWith(GitRepositoryManager.REFS_CACHE_AUTOMERGE)) {
+ continue;
+ } else if (PatchSet.isRef(ref.getName())) {
// Reference to a patch set is visible if the change is visible.
//
if (visibleChanges.contains(Change.Id.fromRef(ref.getName()))) {
@@ -92,8 +103,10 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
// to identify what tags we can actually reach, and what we cannot.
//
if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
- TagMatcher tags = tagCache.get(projectName).
- matcher(db, filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
+ TagMatcher tags = tagCache.get(projectName).matcher(
+ tagCache,
+ db,
+ filterTagsSeperately ? filter(refs).values() : result.values());
for (Ref tag : deferredTags) {
if (tags.isReachable(tag)) {
result.put(tag.getName(), tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 987ab7c021..bb11e6252e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.util.IdGenerator;
@@ -172,6 +172,10 @@ public class WorkQueue {
);
}
+ public void unregisterWorkQueue() {
+ queues.remove(this);
+ }
+
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(
final Runnable runnable, RunnableScheduledFuture<V> r) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
new file mode 100644
index 0000000000..a73f1cb90f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
@@ -0,0 +1,82 @@
+// 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.ioutil;
+
+import com.google.gerrit.server.StringUtil;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple output formatter for column-oriented data, writing its output to
+ * a {@link java.io.PrintWriter} object. Handles escaping of the column
+ * data so that the resulting output is unambiguous and reasonably safe and
+ * machine parsable.
+ */
+public class ColumnFormatter {
+ private char columnSeparator;
+ private boolean firstColumn;
+ private final PrintWriter out;
+
+ /**
+ * @param out The writer to which output should be sent.
+ * @param columnSeparator A character that should serve as the separator
+ * token between columns of output. As only non-printable characters
+ * in the column text are ever escaped, the column separator must be
+ * a non-printable character if the output needs to be unambiguously
+ * parsed.
+ */
+ public ColumnFormatter(final PrintWriter out, final char columnSeparator) {
+ this.out = out;
+ this.columnSeparator = columnSeparator;
+ this.firstColumn = true;
+ }
+
+ /**
+ * Adds a text string as a new column in the current line of output,
+ * taking care of escaping as necessary.
+ *
+ * @param content the string to add.
+ */
+ public void addColumn(final String content) {
+ if (!firstColumn) {
+ out.print(columnSeparator);
+ }
+ out.print(StringUtil.escapeString(content));
+ firstColumn = false;
+ }
+
+ /**
+ * Finishes the output by flushing the current line and takes care of any
+ * other cleanup action.
+ */
+ public void finish() {
+ nextLine();
+ out.flush();
+ }
+
+ /**
+ * Flushes the current line of output and makes the formatter ready to
+ * start receiving new column data for a new line (or end-of-file).
+ * If the current line is empty nothing is done, i.e. consecutive calls
+ * to this method without intervening calls to {@link #addColumn} will
+ * be squashed.
+ */
+ public void nextLine() {
+ if (!firstColumn) {
+ out.print('\n');
+ firstColumn = true;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 7fabcfe1eb..0eb3dfe367 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@ public class AbandonedSender extends ReplyToChangeSender {
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 624e626b7b..4e9ed2b830 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -55,6 +55,19 @@ public class Address {
}
@Override
+ public int hashCode() {
+ return email.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof Address) {
+ return email.equals(((Address) other).email);
+ }
+ return false;
+ }
+
+ @Override
public String toString() {
try {
return toHeaderString();
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 ff0bb57851..6c33949df5 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
@@ -14,39 +14,53 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
import com.google.gwtorm.server.OrmException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
-import java.util.List;
+import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
/** Sends an email to one or more interested parties. */
public abstract class ChangeEmail extends OutgoingEmail {
+ private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
+
protected final Change change;
protected PatchSet patchSet;
protected PatchSetInfo patchSetInfo;
@@ -151,7 +165,6 @@ public abstract class ChangeEmail extends OutgoingEmail {
private void setListIdHeader() throws EmailException {
// Set a reasonable list id so that filters can be used to sort messages
- setVHeader("Mailing-List", "list $email.listId");
setVHeader("List-Id", "<$email.listId.replace('@', '.')>");
if (getSettingsUrl() != null) {
setVHeader("List-Unsubscribe", "<$email.settingsUrl>");
@@ -224,42 +237,46 @@ public abstract class ChangeEmail extends OutgoingEmail {
/** Create the change message and the affected file list. */
public String getChangeDetail() {
- StringBuilder detail = new StringBuilder();
+ try {
+ StringBuilder detail = new StringBuilder();
- if (patchSetInfo != null) {
- detail.append(patchSetInfo.getMessage().trim() + "\n");
- } else {
- detail.append(change.getSubject().trim() + "\n");
- }
+ if (patchSetInfo != null) {
+ detail.append(patchSetInfo.getMessage().trim() + "\n");
+ } else {
+ detail.append(change.getSubject().trim() + "\n");
+ }
- if (patchSet != null) {
- detail.append("---\n");
- PatchList patchList = getPatchList();
- for (PatchListEntry p : patchList.getPatches()) {
- if (Patch.COMMIT_MSG.equals(p.getNewName())) {
- continue;
+ if (patchSet != null) {
+ detail.append("---\n");
+ PatchList patchList = getPatchList();
+ for (PatchListEntry p : patchList.getPatches()) {
+ if (Patch.COMMIT_MSG.equals(p.getNewName())) {
+ continue;
+ }
+ detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
}
- detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+ detail.append(MessageFormat.format("" //
+ + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
+ + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
+ + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
+ + "\n", patchList.getPatches().size() - 1, //
+ patchList.getInsertions(), //
+ patchList.getDeletions()));
+ detail.append("\n");
}
- detail.append(MessageFormat.format("" //
- + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
- + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
- + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
- + "\n", patchList.getPatches().size() - 1, //
- patchList.getInsertions(), //
- patchList.getDeletions()));
- detail.append("\n");
+ return detail.toString();
+ } catch (Exception err) {
+ log.warn("Cannot format change detail", err);
+ return "";
}
- return detail.toString();
}
-
/** Get the patch list corresponding to this patch set. */
- protected PatchList getPatchList() {
+ protected PatchList getPatchList() throws PatchListNotAvailableException {
if (patchSet != null) {
return args.patchListCache.get(change, patchSet);
}
- return null;
+ throw new PatchListNotAvailableException("no patchSet specified");
}
/** Get the project entity the change is in; null if its been deleted. */
@@ -295,53 +312,149 @@ public abstract class ChangeEmail extends OutgoingEmail {
// Just don't BCC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC users that starred updated change", err);
}
}
- /** BCC any user who has set "notify all comments" on this project. */
- protected void bccWatchesNotifyAllComments() {
+ /** BCC users and groups that want notification of events. */
+ protected void bccWatches(NotifyType type) {
try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.ALL_COMMENTS)) {
- add(RecipientType.BCC, w.getAccountId());
- }
+ Watchers matching = getWatches(type);
+ for (Account.Id user : matching.accounts) {
+ add(RecipientType.BCC, user);
+ }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for " + type, err);
}
}
/** Returns all watches that are relevant */
- protected final List<AccountProjectWatch> getWatches() throws OrmException {
+ protected final Watchers getWatches(NotifyType type) throws OrmException {
+ Watchers matching = new Watchers();
if (changeData == null) {
- return Collections.emptyList();
+ return matching;
}
- List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(change.getProject())) {
projectWatchers.add(w.getAccountId());
- add(matching, w);
+ if (w.isNotify(type)) {
+ add(matching, w);
+ }
}
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(args.allProjectsName)) {
- if (!projectWatchers.contains(w.getAccountId())) {
+ if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
add(matching, w);
}
}
- return Collections.unmodifiableList(matching);
+ ProjectState state = projectState;
+ while (state != null) {
+ for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+ if (nc.isNotify(type)) {
+ try {
+ add(matching, nc, state.getProject().getNameKey());
+ } catch (QueryParseException e) {
+ log.warn(String.format(
+ "Project %s has invalid notify %s filter \"%s\": %s",
+ state.getProject().getName(), nc.getName(),
+ nc.getFilter(), e.getMessage()));
+ }
+ }
+ }
+ state = state.getParentState();
+ }
+
+ return matching;
+ }
+
+ protected static class Watchers {
+ protected final Set<Account.Id> accounts = Sets.newHashSet();
+ protected final Set<Address> emails = Sets.newHashSet();
+ }
+
+ @SuppressWarnings("unchecked")
+ private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+ throws OrmException, QueryParseException {
+ for (GroupReference ref : nc.getGroups()) {
+ AccountGroup group =
+ GroupDescriptions.toAccountGroup(args.groupBackend.get(ref.getUUID()));
+ if (group == null) {
+ log.warn(String.format(
+ "Project %s has invalid group %s in notify section %s",
+ project.get(), ref.getName(), nc.getName()));
+ continue;
+ }
+
+ if (group.getType() != AccountGroup.Type.INTERNAL) {
+ log.warn(String.format(
+ "Project %s cannot use group %s of type %s in notify section %s",
+ project.get(), ref.getName(), group.getType(), nc.getName()));
+ continue;
+ }
+
+ ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
+ args.capabilityControlFactory,
+ ref.getUUID()));
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.is_visible();
+ if (nc.getFilter() != null) {
+ p = Predicate.and(qb.parse(nc.getFilter()), p);
+ p = args.queryRewriter.get().rewrite(p);
+ }
+ if (p.match(changeData)) {
+ recursivelyAddAllAccounts(matching, group);
+ }
+ }
+
+ if (!nc.getAddresses().isEmpty()) {
+ if (nc.getFilter() != null) {
+ ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.parse(nc.getFilter());
+ p = args.queryRewriter.get().rewrite(p);
+ if (p.match(changeData)) {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ } else {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ }
+ }
+
+ private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
+ throws OrmException {
+ Set<AccountGroup.Id> seen = Sets.newHashSet();
+ Queue<AccountGroup.Id> scan = Lists.newLinkedList();
+ scan.add(group.getId());
+ seen.add(group.getId());
+ while (!scan.isEmpty()) {
+ AccountGroup.Id next = scan.remove();
+ for (AccountGroupMember m : args.db.get().accountGroupMembers()
+ .byGroup(next)) {
+ matching.accounts.add(m.getAccountId());
+ }
+ for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+ .byGroup(next)) {
+ if (seen.add(m.getIncludeId())) {
+ scan.add(m.getIncludeId());
+ }
+ }
+ }
}
@SuppressWarnings("unchecked")
- private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+ private void add(Watchers matching, AccountProjectWatch w)
throws OrmException {
IdentifiedUser user =
args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -353,13 +466,13 @@ public abstract class ChangeEmail extends OutgoingEmail {
p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
} else if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index f054ee8949..e7cc1ff86b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -17,13 +17,14 @@ package com.google.gerrit.server.mail;
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.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.patch.PatchFile;
import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
@@ -68,7 +69,7 @@ public class CommentSender extends ReplyToChangeSender {
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
@@ -77,11 +78,22 @@ public class CommentSender extends ReplyToChangeSender {
}
public String getInlineComments() {
+ return getInlineComments(1);
+ }
+
+ public String getInlineComments(int lines) {
StringBuilder cmts = new StringBuilder();
final Repository repo = getRepository();
try {
- final PatchList patchList = repo != null ? getPatchList() : null;
+ PatchList patchList = null;
+ if (repo != null) {
+ try {
+ patchList = getPatchList();
+ } catch (PatchListNotAvailableException e) {
+ patchList = null;
+ }
+ }
Patch.Key currentFileKey = null;
PatchFile currentFileData = null;
@@ -113,19 +125,29 @@ public class CommentSender extends ReplyToChangeSender {
}
}
- cmts.append("Line " + lineNbr);
if (currentFileData != null) {
+ int maxLines;
try {
- final String lineStr = currentFileData.getLine(side, lineNbr);
- cmts.append(": ");
- cmts.append(lineStr);
- } catch (Throwable cce) {
- // Don't quote the line if we can't safely convert it.
+ maxLines = currentFileData.getLineCount(side);
+ } catch (Throwable e) {
+ maxLines = lineNbr;
+ }
+
+ final int startLine = Math.max(1, lineNbr - lines + 1);
+ final int stopLine = Math.min(maxLines, lineNbr + lines);
+
+ for (int line = startLine; line <= lineNbr; ++line) {
+ appendFileLine(cmts, currentFileData, side, line);
+ }
+
+ cmts.append(c.getMessage().trim());
+ cmts.append("\n");
+
+ for (int line = lineNbr + 1; line < stopLine; ++line) {
+ appendFileLine(cmts, currentFileData, side, line);
}
}
- cmts.append("\n");
- cmts.append(c.getMessage().trim());
cmts.append("\n\n");
}
} finally {
@@ -136,10 +158,22 @@ public class CommentSender extends ReplyToChangeSender {
return cmts.toString();
}
+ private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
+ cmts.append("Line " + line);
+ try {
+ final String lineStr = fileData.getLine(side, line);
+ cmts.append(": ");
+ cmts.append(lineStr);
+ } catch (Throwable e) {
+ // Don't quote the line if we can't safely convert it.
+ }
+ cmts.append("\n");
+ }
+
private Repository getRepository() {
try {
return args.server.openRepository(projectState.getProject().getNameKey());
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
return null;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c6f716ddae..9b82c715a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,74 +15,65 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.util.HashSet;
-import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Notify interested parties of a brand new change. */
public class CreateChangeSender extends NewChangeSender {
+ private static final Logger log =
+ LoggerFactory.getLogger(CreateChangeSender.class);
+
public static interface Factory {
public CreateChangeSender create(Change change);
}
- private final GroupCache groupCache;
-
@Inject
public CreateChangeSender(EmailArguments ea,
@AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
- GroupCache groupCache, @Assisted Change c) {
+ @Assisted Change c) {
super(ea, anonymousCowardName, sshInfo, c);
- this.groupCache = groupCache;
}
@Override
protected void init() throws EmailException {
super.init();
- bccWatchers();
- }
-
- private void bccWatchers() {
try {
+ // BCC anyone who has interest in this project's changes
// Try to mark interested owners with a TO and not a BCC line.
//
- final Set<Account.Id> owners = new HashSet<Account.Id>();
- for (AccountGroup.UUID uuid : getProjectOwners()) {
- AccountGroup group = groupCache.get(uuid);
- if (group != null) {
- for (AccountGroupMember m : args.db.get().accountGroupMembers()
- .byGroup(group.getId())) {
- owners.add(m.getAccountId());
- }
+ Watchers matching = getWatches(NotifyType.NEW_CHANGES);
+ for (Account.Id user : matching.accounts) {
+ if (isOwnerOfProjectOrBranch(user)) {
+ add(RecipientType.TO, user);
+ } else {
+ add(RecipientType.BCC, user);
}
}
-
- // BCC anyone who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.NEW_CHANGES)) {
- if (owners.contains(w.getAccountId())) {
- add(RecipientType.TO, w.getAccountId());
- } else {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for new change", err);
}
}
+
+ private boolean isOwnerOfProjectOrBranch(Account.Id user) {
+ return projectState != null
+ && change != null
+ && projectState.controlFor(args.identifiedUserFactory.create(user))
+ .controlForRef(change.getDest())
+ .isOwner();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68f78d0a57..e6eda8242e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,10 +15,12 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
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.GroupCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -37,13 +39,15 @@ import javax.annotation.Nullable;
class EmailArguments {
final GitRepositoryManager server;
final ProjectCache projectCache;
- final GroupCache groupCache;
+ final GroupBackend groupBackend;
final AccountCache accountCache;
final PatchListCache patchListCache;
final FromAddressGenerator fromAddressGenerator;
final EmailSender emailSender;
final PatchSetInfoFactory patchSetInfoFactory;
final IdentifiedUser.GenericFactory identifiedUserFactory;
+ final CapabilityControl.Factory capabilityControlFactory;
+ final AnonymousUser anonymousUser;
final Provider<String> urlProvider;
final AllProjectsName allProjectsName;
@@ -51,32 +55,39 @@ class EmailArguments {
final Provider<ChangeQueryRewriter> queryRewriter;
final Provider<ReviewDb> db;
final RuntimeInstance velocityRuntime;
+ final EmailSettings settings;
@Inject
EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
- GroupCache groupCache, AccountCache accountCache,
+ GroupBackend groupBackend, AccountCache accountCache,
PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
GenericFactory identifiedUserFactory,
+ CapabilityControl.Factory capabilityControlFactory,
+ AnonymousUser anonymousUser,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
AllProjectsName allProjectsName,
ChangeQueryBuilder.Factory queryBuilder,
Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
- RuntimeInstance velocityRuntime) {
+ RuntimeInstance velocityRuntime,
+ EmailSettings settings) {
this.server = server;
this.projectCache = projectCache;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.accountCache = accountCache;
this.patchListCache = patchListCache;
this.fromAddressGenerator = fromAddressGenerator;
this.emailSender = emailSender;
this.patchSetInfoFactory = patchSetInfoFactory;
this.identifiedUserFactory = identifiedUserFactory;
+ this.capabilityControlFactory = capabilityControlFactory;
+ this.anonymousUser = anonymousUser;
this.urlProvider = urlProvider;
this.allProjectsName = allProjectsName;
this.queryBuilder = queryBuilder;
this.queryRewriter = queryRewriter;
this.db = db;
this.velocityRuntime = velocityRuntime;
+ this.settings = settings;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
new file mode 100644
index 0000000000..7e44877d7b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -0,0 +1,33 @@
+// 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.mail;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+class EmailSettings {
+ final boolean includeDiff;
+ final int maximumDiffSize;
+
+ @Inject
+ EmailSettings(@GerritServerConfig Config cfg) {
+ includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
+ maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 43078544f8..8501426178 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -40,6 +40,8 @@ public interface EmailTokenVerifier {
/** Exception thrown when a token does not parse correctly. */
public static class InvalidTokenException extends Exception {
+ private static final long serialVersionUID = 1L;
+
public InvalidTokenException() {
super("Invalid token");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index bb503743c2..86cebdce60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.common.base.Charsets;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.GerritPersonIdent;
@@ -24,9 +25,13 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
/** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
@Singleton
public class FromAddressGeneratorProvider implements
@@ -121,7 +126,7 @@ public class FromAddressGeneratorProvider implements
}
static final class PatternGen implements FromAddressGenerator {
- private final String senderEmail;
+ private final ParameterizedString senderEmailPattern;
private final Address serverAddress;
private final AccountCache accountCache;
private final String anonymousCowardName;
@@ -130,7 +135,7 @@ public class FromAddressGeneratorProvider implements
PatternGen(final Address serverAddress, final AccountCache accountCache,
final String anonymousCowardName,
final ParameterizedString namePattern, final String senderEmail) {
- this.senderEmail = senderEmail;
+ this.senderEmailPattern = new ParameterizedString(senderEmail);
this.serverAddress = serverAddress;
this.accountCache = accountCache;
this.anonymousCowardName = anonymousCowardName;
@@ -158,7 +163,25 @@ public class FromAddressGeneratorProvider implements
senderName = serverAddress.name;
}
+ String senderEmail;
+ if (senderEmailPattern.getParameterNames().isEmpty()) {
+ senderEmail = senderEmailPattern.getRawPattern();
+ } else {
+ senderEmail = senderEmailPattern
+ .replace("userHash", hashOf(senderName))
+ .toString();
+ }
return new Address(senderName, senderEmail);
}
}
+
+ private static String hashOf(String data) {
+ try {
+ MessageDigest hash = MessageDigest.getInstance("MD5");
+ byte[] bytes = hash.digest(data.getBytes(Charsets.UTF_8));
+ return Base64.encodeBase64URLSafeString(bytes);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("No MD5 available", e);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 3590b8a90a..70b2d7fa2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -17,7 +17,6 @@ package com.google.gerrit.server.mail;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Change;
@@ -53,8 +52,8 @@ public class MergedSender extends ReplyToChangeSender {
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
- bccWatchesNotifySubmittedChanges();
+ bccWatches(NotifyType.ALL_COMMENTS);
+ bccWatches(NotifyType.SUBMITTED_CHANGES);
}
@Override
@@ -140,20 +139,4 @@ public class MergedSender extends ReplyToChangeSender {
}
m.put(ca.getCategoryId(), ca);
}
-
- private void bccWatchesNotifySubmittedChanges() {
- try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
- } catch (OrmException err) {
- // Just don't CC everyone. Better to send a partial message to those
- // we already have queued up then to fail deliver entirely to people
- // who have a lower interest in the change.
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 2e459d40a1..82c14059a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -16,10 +16,21 @@ package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.ssh.SshInfo;
import com.jcraft.jsch.HostKey;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -28,6 +39,9 @@ import java.util.Set;
/** Sends an email alerting a user to a new change for them to review. */
public abstract class NewChangeSender extends ChangeEmail {
+ private static final Logger log =
+ LoggerFactory.getLogger(NewChangeSender.class);
+
private final SshInfo sshInfo;
private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
@@ -85,4 +99,50 @@ public abstract class NewChangeSender extends ChangeEmail {
}
return host;
}
+
+ public boolean getIncludeDiff() {
+ return args.settings.includeDiff;
+ }
+
+ /** Show patch set as unified difference. */
+ public String getUnifiedDiff() {
+ PatchList patchList;
+ try {
+ patchList = getPatchList();
+ if (patchList.getOldId() == null) {
+ // Octopus merges are not well supported for diff output by Gerrit.
+ // Currently these always have a null oldId in the PatchList.
+ return "";
+ }
+ } catch (PatchListNotAvailableException e) {
+ log.error("Cannot format patch", e);
+ return "";
+ }
+
+ TemporaryBuffer.Heap buf =
+ new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
+ DiffFormatter fmt = new DiffFormatter(buf);
+ Repository git;
+ try {
+ git = args.server.openRepository(change.getProject());
+ } catch (IOException e) {
+ log.error("Cannot open repository to format patch", e);
+ return "";
+ }
+ try {
+ fmt.setRepository(git);
+ fmt.setDetectRenames(true);
+ fmt.format(patchList.getOldId(), patchList.getNewId());
+ return RawParseUtils.decode(buf.toByteArray());
+ } catch (IOException e) {
+ if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+ return "";
+ }
+ log.error("Cannot format patch", e);
+ return "";
+ } finally {
+ fmt.release();
+ git.close();
+ }
+ }
}
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 de8628f990..caa441dfce 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
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
@@ -34,14 +35,13 @@ import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Set;
/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
@@ -53,7 +53,7 @@ public abstract class OutgoingEmail {
protected String messageClass;
private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
private final Map<String, EmailHeader> headers;
- private final List<Address> smtpRcptTo = new ArrayList<Address>();
+ private final Set<Address> smtpRcptTo = Sets.newHashSet();
private Address smtpFromAddress;
private StringBuilder body;
protected VelocityContext velocityContext;
@@ -282,7 +282,7 @@ public abstract class OutgoingEmail {
return false;
}
- if (rcptTo.size() == 1 && rcptTo.contains(fromId)) {
+ if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) {
// If the only recipient is also the sender, don't bother.
//
return false;
@@ -324,14 +324,15 @@ public abstract class OutgoingEmail {
protected void add(final RecipientType rt, final Address addr) {
if (addr != null && addr.email != null && addr.email.length() > 0) {
if (args.emailSender.canEmail(addr.email)) {
- smtpRcptTo.add(addr);
- switch (rt) {
- case TO:
- ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
- break;
- case CC:
- ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
- break;
+ if (smtpRcptTo.add(addr)) {
+ switch (rt) {
+ case TO:
+ ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+ break;
+ case CC:
+ ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+ break;
+ }
}
} else {
log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index c9afddea57..946c29fcda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@ public class RestoredSender extends ReplyToChangeSender {
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 964bfed62b..033bd5656e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -37,7 +38,7 @@ public class RevertedSender extends ReplyToChangeSender {
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index f6817102bc..ce45ffe763 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -182,7 +182,12 @@ public class SmtpEmailSender implements EmailSender {
Writer w = client.sendMessageData();
if (w == null) {
- throw new EmailException("Server " + smtpHost + " rejected body");
+ /* Include rejected recipient error messages here to not lose that
+ * information. That piece of the puzzle is vital if zero recipients
+ * are accepted and the server consequently rejects the DATA command.
+ */
+ throw new EmailException(rejected + "Server " + smtpHost
+ + " rejected DATA command: " + client.getReplyString());
}
w = new BufferedWriter(w);
@@ -201,7 +206,8 @@ public class SmtpEmailSender implements EmailSender {
w.close();
if (!client.completePendingCommand()) {
- throw new EmailException("Server " + smtpHost + " rejected body");
+ throw new EmailException("Server " + smtpHost
+ + " rejected message body: " + client.getReplyString());
}
client.logout();
@@ -237,7 +243,8 @@ public class SmtpEmailSender implements EmailSender {
}
if (!client.login()) {
String e = client.getReplyString();
- throw new EmailException("SMTP server rejected login: " + e);
+ throw new EmailException(
+ "SMTP server rejected HELO/EHLO greeting: " + e);
}
if (smtpEncryption == Encryption.TLS) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index c5c5925348..62ed5e4c14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -29,8 +29,9 @@ import java.io.Serializable;
import java.util.List;
public class IntraLineDiffKey implements Serializable {
- static final long serialVersionUID = 3L;
+ static final long serialVersionUID = 4L;
+ private transient boolean ignoreWhitespace;
private transient ObjectId aId;
private transient ObjectId bId;
@@ -45,7 +46,8 @@ public class IntraLineDiffKey implements Serializable {
private transient String path;
public IntraLineDiffKey(ObjectId aId, Text aText, ObjectId bId, Text bText,
- List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path) {
+ List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path,
+ boolean ignoreWhitespace) {
this.aId = aId;
this.bId = bId;
@@ -56,6 +58,8 @@ public class IntraLineDiffKey implements Serializable {
this.projectKey = projectKey;
this.commit = commit;
this.path = path;
+
+ this.ignoreWhitespace = ignoreWhitespace;
}
Text getTextA() {
@@ -70,11 +74,11 @@ public class IntraLineDiffKey implements Serializable {
return edits;
}
- ObjectId getBlobA() {
+ public ObjectId getBlobA() {
return aId;
}
- ObjectId getBlobB() {
+ public ObjectId getBlobB() {
return bId;
}
@@ -96,6 +100,7 @@ public class IntraLineDiffKey implements Serializable {
h = h * 31 + aId.hashCode();
h = h * 31 + bId.hashCode();
+ h = h * 31 + (ignoreWhitespace ? 1 : 0);
return h;
}
@@ -105,7 +110,8 @@ public class IntraLineDiffKey implements Serializable {
if (o instanceof IntraLineDiffKey) {
final IntraLineDiffKey k = (IntraLineDiffKey) o;
return aId.equals(k.aId) //
- && bId.equals(k.bId);
+ && bId.equals(k.bId) //
+ && ignoreWhitespace == k.ignoreWhitespace;
}
return false;
}
@@ -114,6 +120,9 @@ public class IntraLineDiffKey implements Serializable {
public String toString() {
StringBuilder n = new StringBuilder();
n.append("IntraLineDiffKey[");
+ if (projectKey != null) {
+ n.append(projectKey.get()).append(" ");
+ }
n.append(aId.name());
n.append("..");
n.append(bId.name());
@@ -124,10 +133,12 @@ public class IntraLineDiffKey implements Serializable {
private void writeObject(final ObjectOutputStream out) throws IOException {
writeNotNull(out, aId);
writeNotNull(out, bId);
+ out.writeBoolean(ignoreWhitespace);
}
private void readObject(final ObjectInputStream in) throws IOException {
aId = readNotNull(in);
bId = readNotNull(in);
+ ignoreWhitespace = in.readBoolean();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 358d3baa6e..5b659206f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.patch;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.common.cache.CacheLoader;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -35,9 +35,8 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
-class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
- private static final Logger log = LoggerFactory
- .getLogger(IntraLineLoader.class);
+class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
+ static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
private static final Pattern BLANK_LINE_RE = Pattern
.compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
@@ -62,7 +61,7 @@ class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
}
@Override
- public IntraLineDiff createEntry(IntraLineDiffKey key) throws Exception {
+ public IntraLineDiff load(IntraLineDiffKey key) throws Exception {
Worker w = workerPool.poll();
if (w == null) {
w = new Worker();
@@ -119,7 +118,7 @@ class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
throws Exception {
if (!input.offer(new Input(key))) {
log.error("Cannot enqueue task to thread " + thread.getName());
- return null;
+ return Result.TIMEOUT;
}
Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
index c5ee262f73..f6cff15ac6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// 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.
@@ -12,26 +12,17 @@
// 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.patch;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.common.cache.Weigher;
-/** A disabled {@link ReplicationQueue}. */
-public final class NoReplication implements ReplicationQueue {
+/** Approximates memory usage for IntralineDiff in bytes of memory used. */
+public class IntraLineWeigher implements
+ Weigher<IntraLineDiffKey, IntraLineDiff> {
@Override
- public boolean isEnabled() {
- return false;
- }
-
- @Override
- public void scheduleUpdate(Project.NameKey project, String ref) {
- }
-
- @Override
- public void scheduleFullSync(Project.NameKey project, String urlMatch) {
- }
-
- @Override
- public void replicateNewProject(Project.NameKey project, String head) {
+ public int weigh(IntraLineDiffKey key, IntraLineDiff value) {
+ return 16 + 8*8 + 2*36 // Size of IntraLineDiffKey, 64 bit JVM
+ + 16 + 2*8 + 16+8+4+20 // Size of IntraLineDiff, 64 bit JVM
+ + (8 + 16 + 4*4) * value.getEdits().size();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index f120ebf7b5..6fcf581fba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -111,6 +111,35 @@ public class PatchFile {
}
}
+ /**
+ * Return number of lines in file.
+ *
+ * @param file the file index to extract.
+ * @return number of lines in file.
+ * @throws CorruptEntityException the patch cannot be read.
+ * @throws IOException the patch or complete file content cannot be read.
+ * @throws NoSuchEntityException the file is not exist.
+ */
+ public int getLineCount(final int file)
+ throws CorruptEntityException, IOException, NoSuchEntityException {
+ switch (file) {
+ case 0:
+ if (a == null) {
+ a = load(aTree, entry.getOldName());
+ }
+ return a.size();
+
+ case 1:
+ if (b == null) {
+ b = load(bTree, entry.getNewName());
+ }
+ return b.size();
+
+ default:
+ throw new NoSuchEntityException();
+ }
+ }
+
private Text load(final ObjectId tree, final String path)
throws MissingObjectException, IncorrectObjectTypeException,
CorruptObjectException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 8a61d30016..fe77f5d6ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -19,9 +19,10 @@ import com.google.gerrit.reviewdb.client.PatchSet;
/** Provides a cached list of {@link PatchListEntry}. */
public interface PatchListCache {
- public PatchList get(PatchListKey key);
+ public PatchList get(PatchListKey key) throws PatchListNotAvailableException;
- public PatchList get(Change change, PatchSet patchSet);
+ public PatchList get(Change change, PatchSet patchSet)
+ throws PatchListNotAvailableException;
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 26dbe2dc8b..967e6a7cda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,24 +15,23 @@
package com.google.gerrit.server.patch;
-
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
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.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import java.util.concurrent.ExecutionException;
+
/** Provides a cached list of {@link PatchListEntry}. */
@Singleton
public class PatchListCacheImpl implements PatchListCache {
@@ -43,21 +42,15 @@ public class PatchListCacheImpl implements PatchListCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<PatchListKey, PatchList>> fileType =
- new TypeLiteral<Cache<PatchListKey, PatchList>>() {};
- disk(fileType, FILE_NAME) //
- .memoryLimit(128) // very large items, cache only a few
- .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
- .populateWith(PatchListLoader.class) //
- ;
+ persist(FILE_NAME, PatchListKey.class, PatchList.class)
+ .maximumWeight(10 << 20)
+ .loader(PatchListLoader.class)
+ .weigher(PatchListWeigher.class);
- final TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>> intraType =
- new TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>>() {};
- disk(intraType, INTRA_NAME) //
- .memoryLimit(128) // very large items, cache only a few
- .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
- .populateWith(IntraLineLoader.class) //
- ;
+ persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
+ .maximumWeight(10 << 20)
+ .loader(IntraLineLoader.class)
+ .weigher(IntraLineWeigher.class);
bind(PatchListCacheImpl.class);
bind(PatchListCache.class).to(PatchListCacheImpl.class);
@@ -65,14 +58,14 @@ public class PatchListCacheImpl implements PatchListCache {
};
}
- private final Cache<PatchListKey, PatchList> fileCache;
- private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
+ private final LoadingCache<PatchListKey, PatchList> fileCache;
+ private final LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache;
private final boolean computeIntraline;
@Inject
PatchListCacheImpl(
- @Named(FILE_NAME) final Cache<PatchListKey, PatchList> fileCache,
- @Named(INTRA_NAME) final Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
+ @Named(FILE_NAME) LoadingCache<PatchListKey, PatchList> fileCache,
+ @Named(INTRA_NAME) LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache,
@GerritServerConfig Config cfg) {
this.fileCache = fileCache;
this.intraCache = intraCache;
@@ -82,11 +75,19 @@ public class PatchListCacheImpl implements PatchListCache {
cfg.getBoolean("cache", "diff", "intraline", true));
}
- public PatchList get(final PatchListKey key) {
- return fileCache.get(key);
+ @Override
+ public PatchList get(PatchListKey key) throws PatchListNotAvailableException {
+ try {
+ return fileCache.get(key);
+ } catch (ExecutionException e) {
+ PatchListLoader.log.warn("Error computing " + key, e);
+ throw new PatchListNotAvailableException(e.getCause());
+ }
}
- public PatchList get(final Change change, final PatchSet patchSet) {
+ @Override
+ public PatchList get(final Change change, final PatchSet patchSet)
+ throws PatchListNotAvailableException {
final Project.NameKey projectKey = change.getProject();
final ObjectId a = null;
final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
@@ -97,11 +98,12 @@ public class PatchListCacheImpl implements PatchListCache {
@Override
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
if (computeIntraline) {
- IntraLineDiff d = intraCache.get(key);
- if (d == null) {
- d = new IntraLineDiff(IntraLineDiff.Status.ERROR);
+ try {
+ return intraCache.get(key);
+ } catch (ExecutionException e) {
+ IntraLineLoader.log.warn("Error computing " + key, e);
+ return new IntraLineDiff(IntraLineDiff.Status.ERROR);
}
- return d;
} else {
return new IntraLineDiff(IntraLineDiff.Status.DISABLED);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 33ed54e7ec..ff9e6cf9ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -122,6 +122,22 @@ public class PatchListEntry {
this.deletions = deletions;
}
+ int weigh() {
+ int size = 16 + 6*8 + 2*4 + 20 + 16+8+4+20;
+ size += stringSize(oldName);
+ size += stringSize(newName);
+ size += header.length;
+ size += (8 + 16 + 4*4) * edits.size();
+ return size;
+ }
+
+ private static int stringSize(String str) {
+ if (str != null) {
+ return 16 + 3*4 + 16 + str.length() * 2;
+ }
+ return 0;
+ }
+
public ChangeType getChangeType() {
return changeType;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 5bba42b087..bb231a505f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.patch;
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.common.cache.CacheLoader;
import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
@@ -54,6 +54,8 @@ import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
@@ -62,7 +64,9 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
+class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+ static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
+
private final GitRepositoryManager repoManager;
@Inject
@@ -71,7 +75,7 @@ class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
}
@Override
- public PatchList createEntry(final PatchListKey key) throws Exception {
+ public PatchList load(final PatchListKey key) throws Exception {
final Repository repo = repoManager.openRepository(key.projectKey);
try {
return readPatchList(key, repo);
@@ -251,16 +255,34 @@ class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
ObjectId treeId;
ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
- ObjectInserter ins = m.getObjectInserter();
+ final ObjectInserter ins = repo.newObjectInserter();
try {
DirCache dc = DirCache.newInCore();
m.setDirCache(dc);
+ m.setObjectInserter(new ObjectInserter.Filter() {
+ @Override
+ protected ObjectInserter delegate() {
+ return ins;
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void release() {
+ }
+ });
boolean couldMerge = false;
try {
couldMerge = m.merge(b.getParents());
} catch (IOException e) {
- //
+ // It is not safe to continue further down in this method as throwing
+ // an exception most likely means that the merge tree was not created
+ // and m.getMergeResults() is empty. This would mean that all paths are
+ // unmerged and Gerrit UI would show all paths in the patch list.
+ return null;
}
if (couldMerge) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
new file mode 100644
index 0000000000..2ccc9f1ec4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
@@ -0,0 +1,27 @@
+// 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.patch;
+
+public class PatchListNotAvailableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PatchListNotAvailableException(String message) {
+ super(message);
+ }
+
+ public PatchListNotAvailableException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
new file mode 100644
index 0000000000..d71524622e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -0,0 +1,30 @@
+// 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.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for PatchList in bytes of memory used. */
+public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
+ @Override
+ public int weigh(PatchListKey key, PatchList value) {
+ int size = 16 + 4*8 + 2*36 // Size of PatchListKey, 64 bit JVM
+ + 16 + 3*8 + 3*4 + 20; // Size of PatchList, 64 bit JVM
+ for (PatchListEntry e : value.getPatches()) {
+ size += e.weigh();
+ }
+ return size;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index f59dcc36a9..8165bf2124 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -18,7 +18,6 @@ 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.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -68,31 +67,39 @@ public class PatchSetInfoFactory {
}
public PatchSetInfo get(ReviewDb db, PatchSet.Id patchSetId)
- throws PatchSetInfoNotAvailableException {
- Repository repo = null;
+ throws PatchSetInfoNotAvailableException {
try {
final PatchSet patchSet = db.patchSets().get(patchSetId);
final Change change = db.changes().get(patchSet.getId().getParentKey());
- final Project.NameKey projectKey = change.getProject();
- repo = repoManager.openRepository(projectKey);
+ return get(change, patchSet);
+ } catch (OrmException e) {
+ throw new PatchSetInfoNotAvailableException(e);
+ }
+ }
+
+ public PatchSetInfo get(Change change, PatchSet patchSet)
+ throws PatchSetInfoNotAvailableException {
+ Repository repo;
+ try {
+ repo = repoManager.openRepository(change.getProject());
+ } catch (IOException e) {
+ throw new PatchSetInfoNotAvailableException(e);
+ }
+ try {
final RevWalk rw = new RevWalk(repo);
try {
final RevCommit src =
rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
- PatchSetInfo info = get(src, patchSetId);
+ PatchSetInfo info = get(src, patchSet.getId());
info.setParents(toParentInfos(src.getParents(), rw));
return info;
} finally {
rw.release();
}
- } catch (OrmException e) {
- throw new PatchSetInfoNotAvailableException(e);
} catch (IOException e) {
throw new PatchSetInfoNotAvailableException(e);
} finally {
- if (repo != null) {
- repo.close();
- }
+ repo.close();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
new file mode 100644
index 0000000000..e8af06092c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,393 @@
+// 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.plugins;
+
+import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import org.eclipse.jgit.util.IO;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class AutoRegisterModules {
+ private static final int SKIP_ALL = ClassReader.SKIP_CODE
+ | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
+ private final String pluginName;
+ private final PluginGuiceEnvironment env;
+ private final JarFile jarFile;
+ private final ClassLoader classLoader;
+ private final ModuleGenerator sshGen;
+ private final ModuleGenerator httpGen;
+
+ private Set<Class<?>> sysSingletons;
+ private Multimap<TypeLiteral<?>, Class<?>> sysListen;
+
+ Module sysModule;
+ Module sshModule;
+ Module httpModule;
+
+ AutoRegisterModules(String pluginName,
+ PluginGuiceEnvironment env,
+ JarFile jarFile,
+ ClassLoader classLoader) {
+ this.pluginName = pluginName;
+ this.env = env;
+ this.jarFile = jarFile;
+ this.classLoader = classLoader;
+ this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
+ this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+ }
+
+ AutoRegisterModules discover() throws InvalidPluginException {
+ sysSingletons = Sets.newHashSet();
+ sysListen = LinkedListMultimap.create();
+
+ if (sshGen != null) {
+ sshGen.setPluginName(pluginName);
+ }
+ if (httpGen != null) {
+ httpGen.setPluginName(pluginName);
+ }
+
+ scan();
+
+ if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
+ sysModule = makeSystemModule();
+ }
+ if (sshGen != null) {
+ sshModule = sshGen.create();
+ }
+ if (httpGen != null) {
+ httpModule = httpGen.create();
+ }
+ return this;
+ }
+
+ private Module makeSystemModule() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ for (Class<?> clazz : sysSingletons) {
+ bind(clazz).in(Scopes.SINGLETON);
+ }
+ for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entries()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ Class<Object> impl = (Class<Object>) e.getValue();
+
+ Annotation n = impl.getAnnotation(Export.class);
+ if (n == null) {
+ n = impl.getAnnotation(javax.inject.Named.class);
+ }
+ if (n == null) {
+ n = impl.getAnnotation(com.google.inject.name.Named.class);
+ }
+ if (n == null) {
+ n = UniqueAnnotations.create();
+ }
+ bind(type).annotatedWith(n).to(impl);
+ }
+ }
+ };
+ }
+
+ private void scan() throws InvalidPluginException {
+ Enumeration<JarEntry> e = jarFile.entries();
+ while (e.hasMoreElements()) {
+ JarEntry entry = e.nextElement();
+ if (skip(entry)) {
+ continue;
+ }
+
+ ClassData def = new ClassData();
+ try {
+ new ClassReader(read(entry)).accept(def, SKIP_ALL);
+ } catch (IOException err) {
+ throw new InvalidPluginException("Cannot auto-register", err);
+ } catch (RuntimeException err) {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s has invaild class file %s inside of %s",
+ pluginName, entry.getName(), jarFile.getName()), err);
+ continue;
+ }
+
+ if (def.exportedAsName != null) {
+ if (def.isConcrete()) {
+ export(def);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s tries to @Export(\"%s\") abstract class %s",
+ pluginName, def.exportedAsName, def.className));
+ }
+ } else if (def.listen) {
+ if (def.isConcrete()) {
+ listen(def);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s tries to @Listen abstract class %s",
+ pluginName, def.className));
+ }
+ }
+ }
+ }
+
+ private void export(ClassData def) throws InvalidPluginException {
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(def.className, false, classLoader);
+ } catch (ClassNotFoundException err) {
+ throw new InvalidPluginException(String.format(
+ "Cannot load %s with @Export(\"%s\")",
+ def.className, def.exportedAsName), err);
+ }
+
+ Export export = clazz.getAnnotation(Export.class);
+ if (export == null) {
+ PluginLoader.log.warn(String.format(
+ "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+ pluginName, clazz.getName(), def.exportedAsName));
+ return;
+ }
+
+ if (is("org.apache.sshd.server.Command", clazz)) {
+ if (sshGen != null) {
+ sshGen.export(export, clazz);
+ }
+ } else if (is("javax.servlet.http.HttpServlet", clazz)) {
+ if (httpGen != null) {
+ httpGen.export(export, clazz);
+ listen(clazz, clazz);
+ }
+ } else {
+ int cnt = sysListen.size();
+ listen(clazz, clazz);
+ if (cnt == sysListen.size()) {
+ // If no bindings were recorded, the extension isn't recognized.
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") not supported",
+ clazz.getName(), export.value()));
+ }
+ }
+ }
+
+ private void listen(ClassData def) throws InvalidPluginException {
+ Class<?> clazz;
+ try {
+ clazz = Class.forName(def.className, false, classLoader);
+ } catch (ClassNotFoundException err) {
+ throw new InvalidPluginException(String.format(
+ "Cannot load %s with @Listen",
+ def.className), err);
+ }
+
+ Listen listen = clazz.getAnnotation(Listen.class);
+ if (listen != null) {
+ listen(clazz, clazz);
+ } else {
+ PluginLoader.log.warn(String.format(
+ "In plugin %s asm incorrectly parsed %s with @Listen",
+ pluginName, clazz.getName()));
+ }
+ }
+
+ private void listen(java.lang.reflect.Type type, Class<?> clazz)
+ throws InvalidPluginException {
+ while (type != null) {
+ Class<?> rawType;
+ if (type instanceof ParameterizedType) {
+ rawType = (Class<?>) ((ParameterizedType) type).getRawType();
+ } else if (type instanceof Class) {
+ rawType = (Class<?>) type;
+ } else {
+ return;
+ }
+
+ if (rawType.getAnnotation(ExtensionPoint.class) != null) {
+ TypeLiteral<?> tl = TypeLiteral.get(type);
+ if (env.hasDynamicSet(tl)) {
+ sysSingletons.add(clazz);
+ sysListen.put(tl, clazz);
+ } else if (env.hasDynamicMap(tl)) {
+ if (clazz.getAnnotation(Export.class) == null) {
+ throw new InvalidPluginException(String.format(
+ "Class %s requires @Export(\"name\") annotation for %s",
+ clazz.getName(), rawType.getName()));
+ }
+ sysSingletons.add(clazz);
+ sysListen.put(tl, clazz);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Cannot register %s, server does not accept %s",
+ clazz.getName(), rawType.getName()));
+ }
+ return;
+ }
+
+ java.lang.reflect.Type[] interfaces = rawType.getGenericInterfaces();
+ if (interfaces != null) {
+ for (java.lang.reflect.Type i : interfaces) {
+ listen(i, clazz);
+ }
+ }
+
+ type = rawType.getGenericSuperclass();
+ }
+ }
+
+ private static boolean skip(JarEntry entry) {
+ if (!entry.getName().endsWith(".class")) {
+ return true; // Avoid non-class resources.
+ }
+ if (entry.getSize() <= 0) {
+ return true; // Directories have 0 size.
+ }
+ if (entry.getSize() >= 1024 * 1024) {
+ return true; // Do not scan huge class files.
+ }
+ return false;
+ }
+
+ private byte[] read(JarEntry entry) throws IOException {
+ byte[] data = new byte[(int) entry.getSize()];
+ InputStream in = jarFile.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ return data;
+ }
+
+ private static class ClassData implements ClassVisitor {
+ private static final String EXPORT = Type.getType(Export.class).getDescriptor();
+ private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
+
+ String className;
+ int access;
+ String exportedAsName;
+ boolean listen;
+
+ boolean isConcrete() {
+ return (access & Opcodes.ACC_ABSTRACT) == 0
+ && (access & Opcodes.ACC_INTERFACE) == 0;
+ }
+
+ @Override
+ public void visit(int version, int access, String name, String signature,
+ String superName, String[] interfaces) {
+ this.className = Type.getObjectType(name).getClassName();
+ this.access = access;
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ if (visible && EXPORT.equals(desc)) {
+ return new AbstractAnnotationVisitor() {
+ @Override
+ public void visit(String name, Object value) {
+ exportedAsName = (String) value;
+ }
+ };
+ }
+ if (visible && LISTEN.equals(desc)) {
+ listen = true;
+ return null;
+ }
+ return null;
+ }
+
+ @Override
+ public void visitSource(String arg0, String arg1) {
+ }
+
+ @Override
+ public void visitOuterClass(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public MethodVisitor visitMethod(int arg0, String arg1, String arg2,
+ String arg3, String[] arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
+ }
+
+ @Override
+ public FieldVisitor visitField(int arg0, String arg1, String arg2,
+ String arg3, Object arg4) {
+ return null;
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+
+ @Override
+ public void visitAttribute(Attribute arg0) {
+ }
+ }
+
+ private static abstract class AbstractAnnotationVisitor implements
+ AnnotationVisitor {
+ @Override
+ public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
+ return null;
+ }
+
+ @Override
+ public AnnotationVisitor visitArray(String arg0) {
+ return null;
+ }
+
+ @Override
+ public void visitEnum(String arg0, String arg1, String arg2) {
+ }
+
+ @Override
+ public void visitEnd() {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
new file mode 100644
index 0000000000..7018a3b7f5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -0,0 +1,47 @@
+// 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.plugins;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.jar.JarFile;
+
+class CleanupHandle extends WeakReference<ClassLoader> {
+ private final File tmpFile;
+ private final JarFile jarFile;
+
+ CleanupHandle(File tmpFile,
+ JarFile jarFile,
+ ClassLoader ref,
+ ReferenceQueue<ClassLoader> queue) {
+ super(ref, queue);
+ this.tmpFile = tmpFile;
+ this.jarFile = jarFile;
+ }
+
+ void cleanup() {
+ try {
+ jarFile.close();
+ } catch (IOException err) {
+ }
+ if (!tmpFile.delete() && tmpFile.exists()) {
+ PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+ } else {
+ PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
new file mode 100644
index 0000000000..f34826daca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -0,0 +1,102 @@
+// 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.plugins;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+/**
+ * Copies critical objects from the {@code dbInjector} into a plugin.
+ * <p>
+ * Most explicit bindings are copied automatically from the cfgInjector and
+ * sysInjector to be made available to a plugin's private world. This module is
+ * necessary to get things bound in the dbInjector that are not otherwise easily
+ * available, but that a plugin author might expect to exist.
+ */
+@Singleton
+class CopyConfigModule extends AbstractModule {
+ @Inject
+ @SitePath
+ private File sitePath;
+
+ @Provides
+ @SitePath
+ File getSitePath() {
+ return sitePath;
+ }
+
+ @Inject
+ private SitePaths sitePaths;
+
+ @Provides
+ SitePaths getSitePaths() {
+ return sitePaths;
+ }
+
+ @Inject
+ private TrackingFooters trackingFooters;
+
+ @Provides
+ TrackingFooters getTrackingFooters() {
+ return trackingFooters;
+ }
+
+ @Inject
+ @GerritServerConfig
+ private Config gerritServerConfig;
+
+ @Provides
+ @GerritServerConfig
+ Config getGerritServerConfig() {
+ return gerritServerConfig;
+ }
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Provides
+ SchemaFactory<ReviewDb> getSchemaFactory() {
+ return schemaFactory;
+ }
+
+ @Inject
+ private GitRepositoryManager gitRepositoryManager;
+
+ @Provides
+ GitRepositoryManager getGitRepositoryManager() {
+ return gitRepositoryManager;
+ }
+
+ @Inject
+ CopyConfigModule() {
+ }
+
+ @Override
+ protected void configure() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
new file mode 100644
index 0000000000..31be10cab6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -0,0 +1,27 @@
+// 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.plugins;
+
+public class InvalidPluginException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidPluginException(String message) {
+ super(message);
+ }
+
+ public InvalidPluginException(String message, Throwable why) {
+ super(message, why);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
new file mode 100644
index 0000000000..3f7bc971c0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -0,0 +1,116 @@
+// 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.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/** List the installed plugins. */
+public class ListPlugins {
+ private final PluginLoader pluginLoader;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
+ private boolean all;
+
+ @Inject
+ protected ListPlugins(PluginLoader pluginLoader) {
+ this.pluginLoader = pluginLoader;
+ }
+
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ public ListPlugins setFormat(OutputFormat fmt) {
+ this.format = fmt;
+ return this;
+ }
+
+ public void display(OutputStream out) {
+ final PrintWriter stdout;
+ try {
+ stdout =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(out,
+ "UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ // Our encoding is required by the specifications for the runtime.
+ throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+ }
+
+ Map<String, PluginInfo> output = Maps.newTreeMap();
+
+ List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
+ Collections.sort(plugins, new Comparator<Plugin>() {
+ @Override
+ public int compare(Plugin a, Plugin b) {
+ return a.getName().compareTo(b.getName());
+ }
+ });
+
+ if (!format.isJson()) {
+ stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
+ stdout
+ .print("-------------------------------------------------------------------------------\n");
+ }
+
+ for (Plugin p : plugins) {
+ PluginInfo info = new PluginInfo();
+ info.version = p.getVersion();
+ info.disabled = p.isDisabled() ? true : null;
+
+ if (format.isJson()) {
+ output.put(p.getName(), info);
+ } else {
+ stdout.format("%-30s %-10s %-8s\n", p.getName(),
+ Strings.nullToEmpty(info.version),
+ p.isDisabled() ? "DISABLED" : "ENABLED");
+ }
+ }
+
+ if (format.isJson()) {
+ format.newGson().toJson(output,
+ new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
+ stdout.print('\n');
+ }
+ stdout.flush();
+ }
+
+ private static class PluginInfo {
+ String version;
+ // disabled is only read via reflection when building the json output. We
+ // do not want to show a compiler error that it isn't used.
+ @SuppressWarnings("unused")
+ Boolean disabled;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
new file mode 100644
index 0000000000..92e3b1dd6b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -0,0 +1,26 @@
+// 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.plugins;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+ void setPluginName(String name);
+
+ void export(Export export, Class<?> type) throws InvalidPluginException;
+
+ Module create() throws InvalidPluginException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
new file mode 100644
index 0000000000..91ffbbaf77
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,325 @@
+// 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.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.storage.file.FileSnapshot;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+public class Plugin {
+ public static enum ApiType {
+ EXTENSION, PLUGIN;
+ }
+
+ /** Unique key that changes whenever a plugin reloads. */
+ public static final class CacheKey {
+ private final String name;
+
+ CacheKey(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ int id = System.identityHashCode(this);
+ return String.format("Plugin[%s@%x]", name, id);
+ }
+ }
+
+ static {
+ // Guice logs warnings about multiple injectors being created.
+ // Silence this in case HTTP plugins are used.
+ java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
+ .setLevel(java.util.logging.Level.OFF);
+ }
+
+ static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
+ Attributes main = manifest.getMainAttributes();
+ String v = main.getValue("Gerrit-ApiType");
+ if (Strings.isNullOrEmpty(v)
+ || ApiType.EXTENSION.name().equalsIgnoreCase(v)) {
+ return ApiType.EXTENSION;
+ } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
+ return ApiType.PLUGIN;
+ } else {
+ throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
+ }
+ }
+
+ private final CacheKey cacheKey;
+ private final String name;
+ private final File srcJar;
+ private final FileSnapshot snapshot;
+ private final JarFile jarFile;
+ private final Manifest manifest;
+ private final File dataDir;
+ private final ApiType apiType;
+ private final ClassLoader classLoader;
+ private final boolean disabled;
+ private Class<? extends Module> sysModule;
+ private Class<? extends Module> sshModule;
+ private Class<? extends Module> httpModule;
+
+ private Injector sysInjector;
+ private Injector sshInjector;
+ private Injector httpInjector;
+ private LifecycleManager manager;
+ private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+
+ public Plugin(String name,
+ File srcJar,
+ FileSnapshot snapshot,
+ JarFile jarFile,
+ Manifest manifest,
+ File dataDir,
+ ApiType apiType,
+ ClassLoader classLoader,
+ @Nullable Class<? extends Module> sysModule,
+ @Nullable Class<? extends Module> sshModule,
+ @Nullable Class<? extends Module> httpModule) {
+ this.cacheKey = new CacheKey(name);
+ this.name = name;
+ this.srcJar = srcJar;
+ this.snapshot = snapshot;
+ this.jarFile = jarFile;
+ this.manifest = manifest;
+ this.dataDir = dataDir;
+ this.apiType = apiType;
+ this.classLoader = classLoader;
+ this.disabled = srcJar.getName().endsWith(".disabled");
+ this.sysModule = sysModule;
+ this.sshModule = sshModule;
+ this.httpModule = httpModule;
+ }
+
+ File getSrcJar() {
+ return srcJar;
+ }
+
+ public CacheKey getCacheKey() {
+ return cacheKey;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Nullable
+ public String getVersion() {
+ Attributes main = manifest.getMainAttributes();
+ return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ }
+
+ public ApiType getApiType() {
+ return apiType;
+ }
+
+ boolean canReload() {
+ Attributes main = manifest.getMainAttributes();
+ String v = main.getValue("Gerrit-ReloadMode");
+ if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+ return true;
+ } else if ("restart".equalsIgnoreCase(v)) {
+ return false;
+ } else {
+ PluginLoader.log.warn(String.format(
+ "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+ name, v));
+ return false;
+ }
+ }
+
+ boolean isModified(File jar) {
+ return snapshot.lastModified() != jar.lastModified();
+ }
+
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ public void start(PluginGuiceEnvironment env) throws Exception {
+ Injector root = newRootInjector(env);
+ manager = new LifecycleManager();
+
+ AutoRegisterModules auto = null;
+ if (sysModule == null && sshModule == null && httpModule == null) {
+ auto = new AutoRegisterModules(name, env, jarFile, classLoader);
+ auto.discover();
+ }
+
+ if (sysModule != null) {
+ sysInjector = root.createChildInjector(root.getInstance(sysModule));
+ manager.add(sysInjector);
+ } else if (auto != null && auto.sysModule != null) {
+ sysInjector = root.createChildInjector(auto.sysModule);
+ manager.add(sysInjector);
+ } else {
+ sysInjector = root;
+ }
+
+ if (env.hasSshModule()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getSshModule());
+ }
+ if (sshModule != null) {
+ modules.add(sysInjector.getInstance(sshModule));
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ } else if (auto != null && auto.sshModule != null) {
+ modules.add(auto.sshModule);
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ }
+ }
+
+ if (env.hasHttpModule()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getHttpModule());
+ }
+ if (httpModule != null) {
+ modules.add(sysInjector.getInstance(httpModule));
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ } else if (auto != null && auto.httpModule != null) {
+ modules.add(auto.httpModule);
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ }
+ }
+
+ manager.start();
+ }
+
+ private Injector newRootInjector(final PluginGuiceEnvironment env) {
+ List<Module> modules = Lists.newArrayListWithCapacity(4);
+ modules.add(env.getSysModule());
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getSysModule());
+ } else {
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ServerInformation.class).toInstance(env.getServerInformation());
+ }
+ });
+ }
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(String.class)
+ .annotatedWith(PluginName.class)
+ .toInstance(name);
+
+ bind(File.class)
+ .annotatedWith(PluginData.class)
+ .toProvider(new Provider<File>() {
+ private volatile boolean ready;
+
+ @Override
+ public File get() {
+ if (!ready) {
+ synchronized (dataDir) {
+ if (!dataDir.exists() && !dataDir.mkdirs()) {
+ throw new ProvisionException(String.format(
+ "Cannot create %s for plugin %s",
+ dataDir.getAbsolutePath(), name));
+ }
+ ready = true;
+ }
+ }
+ return dataDir;
+ }
+ });
+ }
+ });
+ return Guice.createInjector(modules);
+ }
+
+ public void stop() {
+ if (manager != null) {
+ manager.stop();
+ manager = null;
+ sysInjector = null;
+ sshInjector = null;
+ httpInjector = null;
+ }
+ }
+
+ public JarFile getJarFile() {
+ return jarFile;
+ }
+
+ public Injector getSysInjector() {
+ return sysInjector;
+ }
+
+ @Nullable
+ public Injector getSshInjector() {
+ return sshInjector;
+ }
+
+ @Nullable
+ public Injector getHttpInjector() {
+ return httpInjector;
+ }
+
+ public void add(RegistrationHandle handle) {
+ if (manager != null) {
+ if (handle instanceof ReloadableRegistrationHandle) {
+ if (reloadableHandles == null) {
+ reloadableHandles = Lists.newArrayList();
+ }
+ reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+ }
+ manager.add(handle);
+ }
+ }
+
+ List<ReloadableRegistrationHandle<?>> getReloadableHandles() {
+ if (reloadableHandles != null) {
+ return reloadableHandles;
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public String toString() {
+ return "Plugin [" + name + "]";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
new file mode 100644
index 0000000000..7081d706d7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -0,0 +1,100 @@
+// 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.plugins;
+
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class PluginCleanerTask implements Runnable {
+ private final WorkQueue workQueue;
+ private final PluginLoader loader;
+ private volatile int pending;
+ private Future<?> self;
+ private int attempts;
+ private long start;
+
+ @Inject
+ PluginCleanerTask(WorkQueue workQueue, PluginLoader loader) {
+ this.workQueue = workQueue;
+ this.loader = loader;
+ }
+
+ @Override
+ public void run() {
+ try {
+ for (int t = 0; t < 2 * (attempts + 1); t++) {
+ System.gc();
+ Thread.sleep(50);
+ }
+ } catch (InterruptedException e) {
+ }
+
+ int left = loader.processPendingCleanups();
+ synchronized (this) {
+ pending = left;
+ self = null;
+
+ if (0 < left) {
+ long waiting = System.currentTimeMillis() - start;
+ PluginLoader.log.warn(String.format(
+ "%d plugins still waiting to be reclaimed after %d minutes",
+ pending,
+ TimeUnit.MILLISECONDS.toMinutes(waiting)));
+ attempts = Math.min(attempts + 1, 15);
+ ensureScheduled();
+ } else {
+ attempts = 0;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ int p = pending;
+ if (0 < p) {
+ return String.format("Plugin Cleaner (waiting for %d plugins)", p);
+ }
+ return "Plugin Cleaner";
+ }
+
+ synchronized void clean(int expect) {
+ if (self == null && pending == 0) {
+ start = System.currentTimeMillis();
+ }
+ pending = expect;
+ ensureScheduled();
+ }
+
+ private void ensureScheduled() {
+ if (self == null && 0 < pending) {
+ if (attempts == 1) {
+ self = workQueue.getDefaultQueue().schedule(
+ this,
+ 30,
+ TimeUnit.SECONDS);
+ } else {
+ self = workQueue.getDefaultQueue().schedule(
+ this,
+ attempts + 1,
+ TimeUnit.MINUTES);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
new file mode 100644
index 0000000000..18460ff1ff
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,494 @@
+// 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.plugins;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
+
+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.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+/**
+ * Tracks Guice bindings that should be exposed to loaded plugins.
+ * <p>
+ * This is an internal implementation detail of how the main server is able to
+ * export its explicit Guice bindings to tightly coupled plugins, giving them
+ * access to singletons and request scoped resources just like any core code.
+ */
+@Singleton
+public class PluginGuiceEnvironment {
+ private final Injector sysInjector;
+ private final ServerInformation srvInfo;
+ private final CopyConfigModule copyConfigModule;
+ private final Set<Key<?>> copyConfigKeys;
+ private final List<StartPluginListener> onStart;
+ private final List<ReloadPluginListener> onReload;
+
+ private Module sysModule;
+ private Module sshModule;
+ private Module httpModule;
+
+ private Provider<ModuleGenerator> sshGen;
+ private Provider<ModuleGenerator> httpGen;
+
+ private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
+ private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
+ private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
+
+ private Map<TypeLiteral<?>, DynamicMap<?>> sysMaps;
+ private Map<TypeLiteral<?>, DynamicMap<?>> sshMaps;
+ private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps;
+
+ @Inject
+ PluginGuiceEnvironment(
+ Injector sysInjector,
+ ServerInformation srvInfo,
+ CopyConfigModule ccm) {
+ this.sysInjector = sysInjector;
+ this.srvInfo = srvInfo;
+ this.copyConfigModule = ccm;
+ this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
+
+ onStart = new CopyOnWriteArrayList<StartPluginListener>();
+ onStart.addAll(listeners(sysInjector, StartPluginListener.class));
+
+ onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
+ onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
+
+ sysSets = dynamicSetsOf(sysInjector);
+ sysMaps = dynamicMapsOf(sysInjector);
+ }
+
+ ServerInformation getServerInformation() {
+ return srvInfo;
+ }
+
+ boolean hasDynamicSet(TypeLiteral<?> type) {
+ return sysSets.containsKey(type)
+ || (sshSets != null && sshSets.containsKey(type))
+ || (httpSets != null && httpSets.containsKey(type));
+ }
+
+ boolean hasDynamicMap(TypeLiteral<?> type) {
+ return sysMaps.containsKey(type)
+ || (sshMaps != null && sshMaps.containsKey(type))
+ || (httpMaps != null && httpMaps.containsKey(type));
+ }
+
+ Module getSysModule() {
+ return sysModule;
+ }
+
+ public void setCfgInjector(Injector cfgInjector) {
+ final Module cm = copy(cfgInjector);
+ final Module sm = copy(sysInjector);
+ sysModule = new AbstractModule() {
+ @Override
+ protected void configure() {
+ install(copyConfigModule);
+ install(cm);
+ install(sm);
+ }
+ };
+ }
+
+ public void setSshInjector(Injector injector) {
+ sshModule = copy(injector);
+ sshGen = injector.getProvider(ModuleGenerator.class);
+ sshSets = dynamicSetsOf(injector);
+ sshMaps = dynamicMapsOf(injector);
+ onStart.addAll(listeners(injector, StartPluginListener.class));
+ onReload.addAll(listeners(injector, ReloadPluginListener.class));
+ }
+
+ boolean hasSshModule() {
+ return sshModule != null;
+ }
+
+ Module getSshModule() {
+ return sshModule;
+ }
+
+ ModuleGenerator newSshModuleGenerator() {
+ return sshGen.get();
+ }
+
+ public void setHttpInjector(Injector injector) {
+ httpModule = copy(injector);
+ httpGen = injector.getProvider(ModuleGenerator.class);
+ httpSets = dynamicSetsOf(injector);
+ httpMaps = dynamicMapsOf(injector);
+ onStart.addAll(listeners(injector, StartPluginListener.class));
+ onReload.addAll(listeners(injector, ReloadPluginListener.class));
+ }
+
+ boolean hasHttpModule() {
+ return httpModule != null;
+ }
+
+ Module getHttpModule() {
+ return httpModule;
+ }
+
+ ModuleGenerator newHttpModuleGenerator() {
+ return httpGen.get();
+ }
+
+ void onStartPlugin(Plugin plugin) {
+ for (StartPluginListener l : onStart) {
+ l.onStartPlugin(plugin);
+ }
+
+ attachSet(sysSets, plugin.getSysInjector(), plugin);
+ attachSet(sshSets, plugin.getSshInjector(), plugin);
+ attachSet(httpSets, plugin.getHttpInjector(), plugin);
+
+ attachMap(sysMaps, plugin.getSysInjector(), plugin);
+ attachMap(sshMaps, plugin.getSshInjector(), plugin);
+ attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+ }
+
+ private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
+ @Nullable Injector src,
+ Plugin plugin) {
+ for (RegistrationHandle h : PrivateInternals_DynamicTypes
+ .attachSets(src, sets)) {
+ plugin.add(h);
+ }
+ }
+
+ private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
+ @Nullable Injector src,
+ Plugin plugin) {
+ for (RegistrationHandle h : PrivateInternals_DynamicTypes
+ .attachMaps(src, plugin.getName(), maps)) {
+ plugin.add(h);
+ }
+ }
+
+ void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+ for (ReloadPluginListener l : onReload) {
+ l.onReloadPlugin(oldPlugin, newPlugin);
+ }
+
+ // Index all old registrations by the raw type. These may be replaced
+ // during the reattach calls below. Any that are not replaced will be
+ // removed when the old plugin does its stop routine.
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> old =
+ LinkedListMultimap.create();
+ for (ReloadableRegistrationHandle<?> h : oldPlugin.getReloadableHandles()) {
+ old.put(h.getKey().getTypeLiteral(), h);
+ }
+
+ reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+ reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+ reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+
+ reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+ reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+ reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+ }
+
+ private void reattachMap(
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+ Map<TypeLiteral<?>, DynamicMap<?>> maps,
+ @Nullable Injector src,
+ Plugin newPlugin) {
+ if (src == null || maps == null || maps.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ PrivateInternals_DynamicMapImpl<Object> map =
+ (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+ Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+ for (ReloadableRegistrationHandle<?> h : oldHandles.get(type)) {
+ Annotation a = h.getKey().getAnnotation();
+ if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+ am.put(a, h);
+ }
+ }
+
+ for (Binding<?> binding : bindings(src, e.getKey())) {
+ @SuppressWarnings("unchecked")
+ Binding<Object> b = (Binding<Object>) binding;
+ Key<Object> key = b.getKey();
+ if (key.getAnnotation() == null) {
+ continue;
+ }
+
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h =
+ (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+ if (h != null) {
+ replace(newPlugin, h, b);
+ oldHandles.remove(type, h);
+ } else {
+ newPlugin.add(map.put(
+ newPlugin.getName(),
+ b.getKey(),
+ b.getProvider()));
+ }
+ }
+ }
+ }
+
+ /** Type used to declare unique annotations. Guice hides this, so extract it. */
+ private static final Class<?> UNIQUE_ANNOTATION =
+ UniqueAnnotations.create().getClass();
+
+ private void reattachSet(
+ ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+ Map<TypeLiteral<?>, DynamicSet<?>> sets,
+ @Nullable Injector src,
+ Plugin newPlugin) {
+ if (src == null || sets == null || sets.isEmpty()) {
+ return;
+ }
+
+ for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+ @SuppressWarnings("unchecked")
+ TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+ @SuppressWarnings("unchecked")
+ DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+ // Index all old handles that match this DynamicSet<T> keyed by
+ // annotations. Ignore the unique annotations, thereby favoring
+ // the @Named annotations or some other non-unique naming.
+ Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+ List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type);
+ Iterator<ReloadableRegistrationHandle<?>> oi = old.iterator();
+ while (oi.hasNext()) {
+ ReloadableRegistrationHandle<?> h = oi.next();
+ Annotation a = h.getKey().getAnnotation();
+ if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+ am.put(a, h);
+ oi.remove();
+ }
+ }
+
+ // Replace old handles with new bindings, favoring cases where there
+ // is an exact match on an @Named annotation. If there is no match
+ // pick any handle and replace it. We generally expect only one
+ // handle of each DynamicSet type when using unique annotations, but
+ // possibly multiple ones if @Named was used. Plugin authors that want
+ // atomic replacement across reloads should use @Named annotations with
+ // stable names that do not change across plugin versions to ensure the
+ // handles are swapped correctly.
+ oi = old.iterator();
+ for (Binding<?> binding : bindings(src, type)) {
+ @SuppressWarnings("unchecked")
+ Binding<Object> b = (Binding<Object>) binding;
+ Key<Object> key = b.getKey();
+ if (key.getAnnotation() == null) {
+ continue;
+ }
+
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h1 =
+ (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+ if (h1 != null) {
+ replace(newPlugin, h1, b);
+ } else if (oi.hasNext()) {
+ @SuppressWarnings("unchecked")
+ ReloadableRegistrationHandle<Object> h2 =
+ (ReloadableRegistrationHandle<Object>) oi.next();
+ oi.remove();
+ replace(newPlugin, h2, b);
+ } else {
+ newPlugin.add(set.add(b.getKey(), b.getProvider()));
+ }
+ }
+ }
+ }
+
+ private static <T> void replace(Plugin newPlugin,
+ ReloadableRegistrationHandle<T> h, Binding<T> b) {
+ RegistrationHandle n = h.replace(b.getKey(), b.getProvider());
+ if (n != null){
+ newPlugin.add(n);
+ }
+ }
+
+ static <T> List<T> listeners(Injector src, Class<T> type) {
+ List<Binding<T>> bindings = bindings(src, TypeLiteral.get(type));
+ int cnt = bindings != null ? bindings.size() : 0;
+ List<T> found = Lists.newArrayListWithCapacity(cnt);
+ if (bindings != null) {
+ for (Binding<T> b : bindings) {
+ found.add(b.getProvider().get());
+ }
+ }
+ return found;
+ }
+
+ private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+ return src.findBindingsByType(type);
+ }
+
+ private Module copy(Injector src) {
+ Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ TypeLiteral<?> type = e.getKey().getTypeLiteral();
+ if (type.getRawType() == DynamicSet.class
+ || type.getRawType() == DynamicMap.class) {
+ ParameterizedType t = (ParameterizedType) type.getType();
+ dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
+ }
+ }
+
+ final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+ for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+ if (dynamicTypes.contains(e.getKey().getTypeLiteral())
+ && e.getKey().getAnnotation() != null) {
+ // A type used in DynamicSet or DynamicMap that has an annotation
+ // must be picked up by the set/map itself. A type used in either
+ // but without an annotation may be magic glue implementing F and
+ // using DynamicSet<F> or DynamicMap<F> internally. That should be
+ // exported to plugins.
+ continue;
+ } else if (shouldCopy(e.getKey())) {
+ bindings.put(e.getKey(), e.getValue());
+ }
+ }
+ bindings.remove(Key.get(Injector.class));
+ bindings.remove(Key.get(java.util.logging.Logger.class));
+
+ return new AbstractModule() {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void configure() {
+ for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) {
+ Key<Object> k = (Key<Object>) e.getKey();
+ Binding<Object> b = (Binding<Object>) e.getValue();
+ bind(k).toProvider(b.getProvider());
+ }
+ }
+ };
+ }
+
+ private boolean shouldCopy(Key<?> key) {
+ if (copyConfigKeys.contains(key)) {
+ return false;
+ }
+ Class<?> type = key.getTypeLiteral().getRawType();
+ if (LifecycleListener.class.isAssignableFrom(type)) {
+ return false;
+ }
+ if (StartPluginListener.class.isAssignableFrom(type)) {
+ return false;
+ }
+
+ if (type.getName().startsWith("com.google.inject.")) {
+ return false;
+ }
+
+ if (is("org.apache.sshd.server.Command", type)) {
+ return false;
+ }
+
+ if (is("javax.servlet.Filter", type)) {
+ return false;
+ }
+ if (is("javax.servlet.ServletContext", type)) {
+ return false;
+ }
+ if (is("javax.servlet.ServletRequest", type)) {
+ return false;
+ }
+ if (is("javax.servlet.ServletResponse", type)) {
+ return false;
+ }
+ if (is("javax.servlet.http.HttpServlet", type)) {
+ return false;
+ }
+ if (is("javax.servlet.http.HttpServletRequest", type)) {
+ return false;
+ }
+ if (is("javax.servlet.http.HttpServletResponse", type)) {
+ return false;
+ }
+ if (is("javax.servlet.http.HttpSession", type)) {
+ return false;
+ }
+ if (Map.class.isAssignableFrom(type)
+ && key.getAnnotationType() != null
+ && "com.google.inject.servlet.RequestParameters"
+ .equals(key.getAnnotationType().getName())) {
+ return false;
+ }
+ if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) {
+ return false;
+ }
+ return true;
+ }
+
+ static boolean is(String name, Class<?> type) {
+ while (type != null) {
+ if (name.equals(type.getName())) {
+ return true;
+ }
+
+ Class<?>[] interfaces = type.getInterfaces();
+ if (interfaces != null) {
+ for (Class<?> i : interfaces) {
+ if (is(name, i)) {
+ return true;
+ }
+ }
+ }
+
+ type = type.getSuperclass();
+ }
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
new file mode 100644
index 0000000000..77fa7028e3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
@@ -0,0 +1,23 @@
+// 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.plugins;
+
+public class PluginInstallException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PluginInstallException(Throwable why) {
+ super(why.getMessage(), why);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
new file mode 100644
index 0000000000..d1c2aa40b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,512 @@
+// 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.plugins;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.ReferenceQueue;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+@Singleton
+public class PluginLoader implements LifecycleListener {
+ static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+
+ private final File pluginsDir;
+ private final File dataDir;
+ private final File tmpDir;
+ private final PluginGuiceEnvironment env;
+ private final ServerInformationImpl srvInfoImpl;
+ private final ConcurrentMap<String, Plugin> running;
+ private final ConcurrentMap<String, Plugin> disabled;
+ private final Map<String, FileSnapshot> broken;
+ private final ReferenceQueue<ClassLoader> cleanupQueue;
+ private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+ private final Provider<PluginCleanerTask> cleaner;
+ private final PluginScannerThread scanner;
+
+ @Inject
+ public PluginLoader(SitePaths sitePaths,
+ PluginGuiceEnvironment pe,
+ ServerInformationImpl sii,
+ Provider<PluginCleanerTask> pct,
+ @GerritServerConfig Config cfg) {
+ pluginsDir = sitePaths.plugins_dir;
+ dataDir = sitePaths.data_dir;
+ tmpDir = sitePaths.tmp_dir;
+ env = pe;
+ srvInfoImpl = sii;
+ running = Maps.newConcurrentMap();
+ disabled = Maps.newConcurrentMap();
+ broken = Maps.newHashMap();
+ cleanupQueue = new ReferenceQueue<ClassLoader>();
+ cleanupHandles = Maps.newConcurrentMap();
+ cleaner = pct;
+
+ long checkFrequency = ConfigUtil.getTimeUnit(cfg,
+ "plugins", null, "checkFrequency",
+ TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS);
+ if (checkFrequency > 0) {
+ scanner = new PluginScannerThread(this, checkFrequency);
+ } else {
+ scanner = null;
+ }
+ }
+
+ public Iterable<Plugin> getPlugins(boolean all) {
+ if (!all) {
+ return running.values();
+ } else {
+ ArrayList<Plugin> plugins = new ArrayList<Plugin>(running.values());
+ plugins.addAll(disabled.values());
+ return plugins;
+ }
+ }
+
+ public void installPluginFromStream(String name, InputStream in)
+ throws IOException, PluginInstallException {
+ if (!name.endsWith(".jar")) {
+ name += ".jar";
+ }
+
+ File jar = new File(pluginsDir, name);
+ name = nameOf(jar);
+
+ File old = new File(pluginsDir, ".last_" + name + ".zip");
+ File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+ synchronized (this) {
+ Plugin active = running.get(name);
+ if (active != null) {
+ log.info(String.format("Replacing plugin %s", name));
+ old.delete();
+ jar.renameTo(old);
+ }
+
+ new File(pluginsDir, name + ".jar.disabled").delete();
+ tmp.renameTo(jar);
+ try {
+ runPlugin(name, jar, active);
+ if (active == null) {
+ log.info(String.format("Installed plugin %s", name));
+ }
+ } catch (PluginInstallException e) {
+ jar.delete();
+ throw e;
+ }
+
+ cleanInBackground();
+ }
+ }
+
+ public static File storeInTemp(String pluginName, InputStream in,
+ SitePaths sitePaths) throws IOException {
+ return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
+ }
+
+ private static File asTemp(InputStream in,
+ String prefix, String suffix,
+ File dir) throws IOException {
+ File tmp = File.createTempFile(prefix, suffix, dir);
+ boolean keep = false;
+ try {
+ FileOutputStream out = new FileOutputStream(tmp);
+ try {
+ byte[] data = new byte[8192];
+ int n;
+ while ((n = in.read(data)) > 0) {
+ out.write(data, 0, n);
+ }
+ keep = true;
+ return tmp;
+ } finally {
+ out.close();
+ }
+ } finally {
+ if (!keep) {
+ tmp.delete();
+ }
+ }
+ }
+
+ public void disablePlugins(Set<String> names) {
+ synchronized (this) {
+ for (String name : names) {
+ Plugin active = running.get(name);
+ if (active == null) {
+ continue;
+ }
+
+ log.info(String.format("Disabling plugin %s", name));
+ File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+ active.getSrcJar().renameTo(off);
+
+ active.stop();
+ running.remove(name);
+ try {
+ FileSnapshot snapshot = FileSnapshot.save(off);
+ Plugin offPlugin = loadPlugin(name, off, snapshot);
+ disabled.put(name, offPlugin);
+ } catch (Throwable e) {
+ // This shouldn't happen, as the plugin was loaded earlier.
+ log.warn(String.format("Cannot load disabled plugin %s", name),
+ e.getCause());
+ }
+ }
+ cleanInBackground();
+ }
+ }
+
+ public void enablePlugins(Set<String> names) throws PluginInstallException {
+ synchronized (this) {
+ for (String name : names) {
+ Plugin off = disabled.get(name);
+ if (off == null) {
+ continue;
+ }
+
+ log.info(String.format("Enabling plugin %s", name));
+ File on = new File(pluginsDir, off.getName() + ".jar");
+ off.getSrcJar().renameTo(on);
+
+ disabled.remove(name);
+ runPlugin(name, on, null);
+ }
+ cleanInBackground();
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+ srvInfoImpl.state = ServerInformation.State.STARTUP;
+ rescan();
+ srvInfoImpl.state = ServerInformation.State.RUNNING;
+ if (scanner != null) {
+ scanner.start();
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (scanner != null) {
+ scanner.end();
+ }
+ srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
+ synchronized (this) {
+ for (Plugin p : running.values()) {
+ p.stop();
+ }
+ running.clear();
+ disabled.clear();
+ broken.clear();
+ if (cleanupHandles.size() > running.size()) {
+ System.gc();
+ processPendingCleanups();
+ }
+ }
+ }
+
+ public void reload(List<String> names)
+ throws InvalidPluginException, PluginInstallException {
+ synchronized (this) {
+ List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
+ List<String> bad = Lists.newArrayListWithExpectedSize(4);
+ for (String name : names) {
+ Plugin active = running.get(name);
+ if (active != null) {
+ reload.add(active);
+ } else {
+ bad.add(name);
+ }
+ }
+ if (!bad.isEmpty()) {
+ throw new InvalidPluginException(String.format(
+ "Plugin(s) \"%s\" not running",
+ Joiner.on("\", \"").join(bad)));
+ }
+
+ for (Plugin active : reload) {
+ String name = active.getName();
+ try {
+ log.info(String.format("Reloading plugin %s", name));
+ runPlugin(name, active.getSrcJar(), active);
+ } catch (PluginInstallException e) {
+ log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+ throw e;
+ }
+ }
+
+ cleanInBackground();
+ }
+ }
+
+ public synchronized void rescan() {
+ List<File> jars = scanJarsInPluginsDirectory();
+ stopRemovedPlugins(jars);
+ dropRemovedDisabledPlugins(jars);
+
+ for (File jar : jars) {
+ String name = nameOf(jar);
+ FileSnapshot brokenTime = broken.get(name);
+ if (brokenTime != null && !brokenTime.isModified(jar)) {
+ continue;
+ }
+
+ Plugin active = running.get(name);
+ if (active != null && !active.isModified(jar)) {
+ continue;
+ }
+
+ if (active != null) {
+ log.info(String.format("Reloading plugin %s", name));
+ }
+
+ try {
+ Plugin loadedPlugin = runPlugin(name, jar, active);
+ if (active == null && !loadedPlugin.isDisabled()) {
+ log.info(String.format("Loaded plugin %s", name));
+ }
+ } catch (PluginInstallException e) {
+ log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+ }
+ }
+
+ cleanInBackground();
+ }
+
+ private Plugin runPlugin(String name, File jar, Plugin oldPlugin)
+ throws PluginInstallException {
+ FileSnapshot snapshot = FileSnapshot.save(jar);
+ try {
+ Plugin newPlugin = loadPlugin(name, jar, snapshot);
+ boolean reload = oldPlugin != null
+ && oldPlugin.canReload()
+ && newPlugin.canReload();
+ if (!reload && oldPlugin != null) {
+ oldPlugin.stop();
+ running.remove(name);
+ }
+ if (!newPlugin.isDisabled()) {
+ newPlugin.start(env);
+ }
+ if (reload) {
+ env.onReloadPlugin(oldPlugin, newPlugin);
+ oldPlugin.stop();
+ } else if (!newPlugin.isDisabled()) {
+ env.onStartPlugin(newPlugin);
+ }
+ if (!newPlugin.isDisabled()) {
+ running.put(name, newPlugin);
+ } else {
+ disabled.put(name, newPlugin);
+ }
+ broken.remove(name);
+ return newPlugin;
+ } catch (Throwable err) {
+ broken.put(name, snapshot);
+ throw new PluginInstallException(err);
+ }
+ }
+
+ private void stopRemovedPlugins(List<File> jars) {
+ Set<String> unload = Sets.newHashSet(running.keySet());
+ for (File jar : jars) {
+ if (!jar.getName().endsWith(".disabled")) {
+ unload.remove(nameOf(jar));
+ }
+ }
+ for (String name : unload){
+ log.info(String.format("Unloading plugin %s", name));
+ running.remove(name).stop();
+ }
+ }
+
+ private void dropRemovedDisabledPlugins(List<File> jars) {
+ Set<String> unload = Sets.newHashSet(disabled.keySet());
+ for (File jar : jars) {
+ if (jar.getName().endsWith(".disabled")) {
+ unload.remove(nameOf(jar));
+ }
+ }
+ for (String name : unload) {
+ disabled.remove(name);
+ }
+ }
+
+ synchronized int processPendingCleanups() {
+ CleanupHandle h;
+ while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
+ h.cleanup();
+ cleanupHandles.remove(h);
+ }
+ return Math.max(0, cleanupHandles.size() - running.size());
+ }
+
+ private void cleanInBackground() {
+ int cnt = Math.max(0, cleanupHandles.size() - running.size());
+ if (0 < cnt) {
+ cleaner.get().clean(cnt);
+ }
+ }
+
+ private static String nameOf(File jar) {
+ String name = jar.getName();
+ if (name.endsWith(".disabled")) {
+ name = name.substring(0, name.lastIndexOf('.'));
+ }
+ int ext = name.lastIndexOf('.');
+ return 0 < ext ? name.substring(0, ext) : name;
+ }
+
+ private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
+ throws IOException, ClassNotFoundException, InvalidPluginException {
+ File tmp;
+ FileInputStream in = new FileInputStream(srcJar);
+ try {
+ tmp = asTemp(in, tempNameFor(name), ".jar", tmpDir);
+ } finally {
+ in.close();
+ }
+
+ JarFile jarFile = new JarFile(tmp);
+ boolean keep = false;
+ try {
+ Manifest manifest = jarFile.getManifest();
+ Plugin.ApiType type = Plugin.getApiType(manifest);
+ Attributes main = manifest.getMainAttributes();
+ String sysName = main.getValue("Gerrit-Module");
+ String sshName = main.getValue("Gerrit-SshModule");
+ String httpName = main.getValue("Gerrit-HttpModule");
+
+ if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
+ throw new InvalidPluginException(String.format(
+ "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+ Plugin.ApiType.PLUGIN));
+ }
+
+ URL[] urls = {tmp.toURI().toURL()};
+ ClassLoader parentLoader = parentFor(type);
+ ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+ cleanupHandles.put(
+ new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
+ Boolean.TRUE);
+
+ Class<? extends Module> sysModule = load(sysName, pluginLoader);
+ Class<? extends Module> sshModule = load(sshName, pluginLoader);
+ Class<? extends Module> httpModule = load(httpName, pluginLoader);
+ keep = true;
+ return new Plugin(name,
+ srcJar, snapshot,
+ jarFile, manifest,
+ new File(dataDir, name), type, pluginLoader,
+ sysModule, sshModule, httpModule);
+ } finally {
+ if (!keep) {
+ jarFile.close();
+ }
+ }
+ }
+
+ private static ClassLoader parentFor(Plugin.ApiType type)
+ throws InvalidPluginException {
+ switch (type) {
+ case EXTENSION:
+ return PluginName.class.getClassLoader();
+ case PLUGIN:
+ return PluginLoader.class.getClassLoader();
+ default:
+ throw new InvalidPluginException("Unsupported ApiType " + type);
+ }
+ }
+
+ private static String tempNameFor(String name) {
+ SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
+ return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
+ }
+
+ private Class<? extends Module> load(String name, ClassLoader pluginLoader)
+ throws ClassNotFoundException {
+ if (Strings.isNullOrEmpty(name)) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ Class<? extends Module> clazz =
+ (Class<? extends Module>) Class.forName(name, false, pluginLoader);
+ if (!Module.class.isAssignableFrom(clazz)) {
+ throw new ClassCastException(String.format(
+ "Class %s does not implement %s",
+ name, Module.class.getName()));
+ }
+ return clazz;
+ }
+
+ private List<File> scanJarsInPluginsDirectory() {
+ if (pluginsDir == null || !pluginsDir.exists()) {
+ return Collections.emptyList();
+ }
+ File[] matches = pluginsDir.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ String n = pathname.getName();
+ return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+ && pathname.isFile();
+ }
+ });
+ if (matches == null) {
+ log.error("Cannot list " + pluginsDir.getAbsolutePath());
+ return Collections.emptyList();
+ }
+ return Arrays.asList(matches);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
new file mode 100644
index 0000000000..ab7dc3c839
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -0,0 +1,33 @@
+// 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.plugins;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.lifecycle.LifecycleModule;
+
+public class PluginModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(ServerInformationImpl.class);
+ bind(ServerInformation.class).to(ServerInformationImpl.class);
+
+ bind(PluginCleanerTask.class);
+ bind(PluginGuiceEnvironment.class);
+ bind(PluginLoader.class);
+
+ bind(CopyConfigModule.class);
+ listener().to(PluginLoader.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
new file mode 100644
index 0000000000..a484c5d44b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -0,0 +1,52 @@
+// 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.plugins;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class PluginScannerThread extends Thread {
+ private final CountDownLatch done = new CountDownLatch(1);
+ private final PluginLoader loader;
+ private final long checkFrequencyMillis;
+
+ PluginScannerThread(PluginLoader loader, long checkFrequencyMillis) {
+ this.loader = loader;
+ this.checkFrequencyMillis = checkFrequencyMillis;
+ setDaemon(true);
+ setName("PluginScanner");
+ }
+
+ @Override
+ public void run() {
+ for (;;) {
+ try {
+ if (done.await(checkFrequencyMillis, TimeUnit.MILLISECONDS)) {
+ return;
+ }
+ } catch (InterruptedException e) {
+ }
+ loader.rescan();
+ }
+ }
+
+ void end() {
+ done.countDown();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
new file mode 100644
index 0000000000..72a499edb7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
@@ -0,0 +1,20 @@
+// 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.plugins;
+
+/** Broadcasts event indicating a plugin was reloaded. */
+public interface ReloadPluginListener {
+ public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
new file mode 100644
index 0000000000..c4a89009f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
@@ -0,0 +1,28 @@
+// 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.plugins;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.Singleton;
+
+@Singleton
+class ServerInformationImpl implements ServerInformation {
+ volatile State state = State.STARTUP;
+
+ @Override
+ public State getState() {
+ return state;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
new file mode 100644
index 0000000000..aaad370009
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
@@ -0,0 +1,20 @@
+// 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.plugins;
+
+/** Broadcasts event indicating a plugin was loaded. */
+public interface StartPluginListener {
+ public void onStartPlugin(Plugin plugin);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index d17762ee15..db4b0217b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -26,10 +26,11 @@ import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.util.Providers;
import com.googlecode.prolog_cafe.compiler.CompileException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -49,6 +50,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import javax.annotation.Nullable;
+
/** Access control management for a user accessing a single change. */
public class ChangeControl {
@@ -161,7 +164,7 @@ public class ChangeControl {
/** Can this user see this change? */
public boolean isVisible(ReviewDb db) throws OrmException {
- if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+ if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, null)) {
return false;
}
return isRefVisible();
@@ -174,7 +177,7 @@ public class ChangeControl {
/** Can this user see the given patchset? */
public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
- if (ps.isDraft() && !isDraftVisible(db)) {
+ if (ps.isDraft() && !isDraftVisible(db, null)) {
return false;
}
return isVisible(db);
@@ -186,6 +189,7 @@ public class ChangeControl {
|| getRefControl().isOwner() // branch owner can abandon
|| getProjectControl().isOwner() // project owner can abandon
|| getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+ || getRefControl().canAbandon() // user can abandon a specific ref
;
}
@@ -207,7 +211,8 @@ public class ChangeControl {
/** Can this user restore this change? */
public boolean canRestore() {
- return canAbandon(); // Anyone who can abandon the change can restore it back
+ return canAbandon() // Anyone who can abandon the change can restore it back
+ && getRefControl().canUpload(); // as long as you can upload too
}
/** All value ranges of any allowed label permission. */
@@ -236,10 +241,20 @@ public class ChangeControl {
/** Is this user a reviewer for the change? */
public boolean isReviewer(ReviewDb db) throws OrmException {
+ return isReviewer(db, null);
+ }
+
+ /** Is this user a reviewer for the change? */
+ public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
+ throws OrmException {
if (getCurrentUser() instanceof IdentifiedUser) {
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
- ResultSet<PatchSetApproval> results =
- db.patchSetApprovals().byChange(change.getId());
+ Iterable<PatchSetApproval> results;
+ if (cd != null) {
+ results = cd.currentApprovals(Providers.of(db));
+ } else {
+ results = db.patchSetApprovals().byChange(change.getId());
+ }
for (PatchSetApproval approval : results) {
if (user.getAccountId().equals(approval.getAccountId())) {
return true;
@@ -279,34 +294,43 @@ public class ChangeControl {
return false;
}
- public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
- if (change.getStatus().isClosed()) {
+ public List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
+ return canSubmit(db, patchSet, null, false, true);
+ }
+
+ public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
+ return canSubmit(db, patchSet, null, false, false);
+ }
+
+ public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
+ @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) {
+ if (!allowClosed && change.getStatus().isClosed()) {
SubmitRecord rec = new SubmitRecord();
rec.status = SubmitRecord.Status.CLOSED;
return Collections.singletonList(rec);
}
- if (!patchSetId.equals(change.currentPatchSetId())) {
- return ruleError("Patch set " + patchSetId + " is not current");
+ if (!patchSet.getId().equals(change.currentPatchSetId())) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
}
try {
- if (change.getStatus() == Change.Status.DRAFT){
- if (!isVisible(db)) {
- return ruleError("Patch set " + patchSetId + " not found");
+ if (change.getStatus() == Change.Status.DRAFT) {
+ if (!isDraftVisible(db, cd)) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
} else {
return ruleError("Cannot submit draft changes");
}
}
- if (isDraftPatchSet(patchSetId, db)) {
- if (!isVisible(db)) {
- return ruleError("Patch set " + patchSetId + " not found");
+ if (patchSet.isDraft()) {
+ if (!isDraftVisible(db, cd)) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
} else {
return ruleError("Cannot submit draft patch sets");
}
}
} catch (OrmException err) {
- return logRuleError("Cannot read patch set " + patchSetId, err);
+ return logRuleError("Cannot read patch set " + patchSet.getId(), err);
}
List<Term> results = new ArrayList<Term>();
@@ -324,7 +348,8 @@ public class ChangeControl {
try {
env.set(StoredValues.REVIEW_DB, db);
env.set(StoredValues.CHANGE, change);
- env.set(StoredValues.PATCH_SET_ID, patchSetId);
+ env.set(StoredValues.CHANGE_DATA, cd);
+ env.set(StoredValues.PATCH_SET, patchSet);
env.set(StoredValues.CHANGE_CONTROL, this);
submitRule = env.once(
@@ -335,6 +360,10 @@ public class ChangeControl {
+ getProject().getName());
}
+ if (fastEvalLabels) {
+ env.once("gerrit", "assume_range_from_label");
+ }
+
try {
for (Term[] template : env.all(
"gerrit", "can_submit",
@@ -373,6 +402,10 @@ public class ChangeControl {
parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
if (filterRule != null) {
try {
+ if (fastEvalLabels) {
+ env.once("gerrit", "assume_range_from_label");
+ }
+
Term resultsTerm = toListTerm(results);
results.clear();
Term[] template = parentEnv.once(
@@ -409,12 +442,18 @@ public class ChangeControl {
return ruleError("Project submit rule has no solution");
}
- // Convert the results from Prolog Cafe's format to Gerrit's common format.
- // can_submit/1 terminates when an ok(P) record is found. Therefore walk
- // the results backwards, using only that ok(P) record if it exists. This
- // skips partial results that occur early in the output. Later after the loop
- // the out collection is reversed to restore it to the original ordering.
- //
+ return resultsToSubmitRecord(submitRule, results);
+ }
+
+ /**
+ * Convert the results from Prolog Cafe's format to Gerrit's common format.
+ *
+ * can_submit/1 terminates when an ok(P) record is found. Therefore walk
+ * the results backwards, using only that ok(P) record if it exists. This
+ * skips partial results that occur early in the output. Later after the loop
+ * the out collection is reversed to restore it to the original ordering.
+ */
+ public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
List<SubmitRecord> out = new ArrayList<SubmitRecord>(results.size());
for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
Term submitRecord = results.get(resultIdx);
@@ -468,6 +507,9 @@ public class ChangeControl {
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
+ } else if ("may".equals(status.name())) {
+ lbl.status = SubmitRecord.Label.Status.MAY;
+
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
@@ -516,16 +558,9 @@ public class ChangeControl {
}
}
- private boolean isDraftVisible(ReviewDb db) throws OrmException {
- return isOwner() || isReviewer(db);
- }
-
- private boolean isDraftPatchSet(PatchSet.Id id, ReviewDb db) throws OrmException {
- PatchSet ps = db.patchSets().get(id);
- if (ps == null) {
- throw new OrmException("Patch set " + id + " not found");
- }
- return ps.isDraft();
+ private boolean isDraftVisible(ReviewDb db, ChangeData cd)
+ throws OrmException {
+ return isOwner() || isReviewer(db, cd);
}
private static boolean isUser(Term who) {
@@ -535,7 +570,7 @@ public class ChangeControl {
&& who.arg(0).isInteger();
}
- private static Term toListTerm(List<Term> terms) {
+ public static Term toListTerm(List<Term> terms) {
Term list = Prolog.Nil;
for (int i = terms.size() - 1; i >= 0; i--) {
list = new ListTerm(terms.get(i), list);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 65bdc1e3dd..3dbd7b7f72 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -15,21 +15,24 @@
package com.google.gerrit.server.project;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.AccountGroup;
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.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.ProjectOwnerGroups;
+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.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -49,6 +52,8 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Set;
@@ -64,28 +69,32 @@ public class CreateProject {
private final Set<AccountGroup.UUID> projectOwnerGroups;
private final IdentifiedUser currentUser;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated referenceUpdated;
+ private final DynamicSet<NewProjectCreatedListener> createdListener;
private final PersonIdent serverIdent;
private CreateProjectArgs createProjectArgs;
private ProjectCache projectCache;
- private GroupCache groupCache;
+ private GroupBackend groupBackend;
private MetaDataUpdate.User metaDataUpdateFactory;
@Inject
CreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
- ReplicationQueue replicateq, ReviewDb db,
- @GerritPersonIdent PersonIdent personIdent, final GroupCache groupCache,
- final MetaDataUpdate.User metaDataUpdateFactory,
+ GitReferenceUpdated referenceUpdated,
+ DynamicSet<NewProjectCreatedListener> createdListener,
+ ReviewDb db,
+ @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+ MetaDataUpdate.User metaDataUpdateFactory,
@Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
this.projectOwnerGroups = pOwnerGroups;
this.currentUser = identifiedUser;
this.repoManager = gitRepoManager;
- this.replication = replicateq;
+ this.referenceUpdated = referenceUpdated;
+ this.createdListener = createdListener;
this.serverIdent = personIdent;
this.createProjectArgs = createPArgs;
this.projectCache = pCache;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
}
@@ -95,10 +104,23 @@ public class CreateProject {
try {
final String head =
createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
- : createProjectArgs.branch;
+ : createProjectArgs.branch.get(0);
final Repository repo = repoManager.createRepository(nameKey);
try {
- replication.replicateNewProject(nameKey, head);
+ NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+ @Override
+ public String getProjectName() {
+ return nameKey.get();
+ }
+
+ @Override
+ public String getHeadName() {
+ return head;
+ }
+ };
+ for (NewProjectCreatedListener l : createdListener) {
+ l.onNewProjectCreated(event);
+ }
final RefUpdate u = repo.updateRef(Constants.HEAD);
u.disableRefLog();
@@ -108,7 +130,7 @@ public class CreateProject {
if (!createProjectArgs.permissionsOnly
&& createProjectArgs.createEmptyCommit) {
- createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+ createEmptyCommits(repo, nameKey, createProjectArgs.branch);
}
} finally {
repo.close();
@@ -130,10 +152,10 @@ public class CreateProject {
} finally {
repo.close();
}
- } catch (RepositoryNotFoundException doesNotExist) {
+ } catch (IOException ioErr) {
final String msg = "Cannot create " + nameKey;
log.error(msg, err);
- throw new ProjectCreationFailedException(msg, err);
+ throw new ProjectCreationFailedException(msg, ioErr);
}
} catch (Exception e) {
final String msg = "Cannot create " + nameKey;
@@ -166,9 +188,9 @@ public class CreateProject {
final AccessSection all =
config.getAccessSection(AccessSection.ALL, true);
for (AccountGroup.UUID ownerId : createProjectArgs.ownerIds) {
- AccountGroup accountGroup = groupCache.get(ownerId);
- if (accountGroup != null) {
- GroupReference group = config.resolve(accountGroup);
+ GroupDescription.Basic g = groupBackend.get(ownerId);
+ if (g != null) {
+ GroupReference group = config.resolve(GroupReference.forGroup(g));
all.getPermission(Permission.OWNER, true).add(
new PermissionRule(group));
}
@@ -176,17 +198,14 @@ public class CreateProject {
}
md.setMessage("Created project\n");
- if (!config.commit(md)) {
- throw new IOException("Cannot create "
- + createProjectArgs.getProjectName());
- }
+ config.commit(md);
} finally {
md.close();
}
projectCache.onCreateProject(createProjectArgs.getProject());
repoManager.setProjectDescription(createProjectArgs.getProject(),
createProjectArgs.projectDescription);
- replication.scheduleUpdate(createProjectArgs.getProject(),
+ referenceUpdated.fire(createProjectArgs.getProject(),
GitRepositoryManager.REF_CONFIG);
}
@@ -216,20 +235,32 @@ public class CreateProject {
new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
}
- while (createProjectArgs.branch.startsWith("/")) {
- createProjectArgs.branch = createProjectArgs.branch.substring(1);
- }
- if (!createProjectArgs.branch.startsWith(Constants.R_HEADS)) {
- createProjectArgs.branch = Constants.R_HEADS + createProjectArgs.branch;
+ List<String> transformedBranches = new ArrayList<String>();
+ if (createProjectArgs.branch == null ||
+ createProjectArgs.branch.isEmpty()) {
+ createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
}
- if (!Repository.isValidRefName(createProjectArgs.branch)) {
- throw new ProjectCreationFailedException(String.format(
- "Branch \"%s\" is not a valid name.", createProjectArgs.branch));
+ for (String branch : createProjectArgs.branch) {
+ while (branch.startsWith("/")) {
+ branch = branch.substring(1);
+ }
+ if (!branch.startsWith(Constants.R_HEADS)) {
+ branch = Constants.R_HEADS + branch;
+ }
+ if (!Repository.isValidRefName(branch)) {
+ throw new ProjectCreationFailedException(String.format(
+ "Branch \"%s\" is not a valid name.", branch));
+ }
+ if (!transformedBranches.contains(branch)) {
+ transformedBranches.add(branch);
+ }
}
+ createProjectArgs.branch = transformedBranches;
}
- private void createEmptyCommit(final Repository repo,
- final Project.NameKey project, final String ref) throws IOException {
+ private void createEmptyCommits(final Repository repo,
+ final Project.NameKey project, final List<String> refs)
+ throws IOException {
ObjectInserter oi = repo.newObjectInserter();
try {
CommitBuilder cb = new CommitBuilder();
@@ -241,15 +272,18 @@ public class CreateProject {
ObjectId id = oi.insert(cb);
oi.flush();
- RefUpdate ru = repo.updateRef(Constants.HEAD);
- ru.setNewObjectId(id);
- final Result result = ru.update();
- switch (result) {
- case NEW:
- replication.scheduleUpdate(project, ref);
- break;
- default: {
- throw new IOException(result.name());
+ for (String ref : refs) {
+ RefUpdate ru = repo.updateRef(ref);
+ ru.setNewObjectId(id);
+ final Result result = ru.update();
+ switch (result) {
+ case NEW:
+ referenceUpdated.fire(project, ref);
+ break;
+ default: {
+ throw new IOException(String.format(
+ "Failed to create ref \"%s\": %s", ref, result.name()));
+ }
}
}
} catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 98adf8598e..2dee4f4976 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -30,7 +30,7 @@ public class CreateProjectArgs {
public boolean contributorAgreements;
public boolean signedOffBy;
public boolean permissionsOnly;
- public String branch;
+ public List<String> branch;
public boolean contentMerge;
public boolean changeIdRequired;
public boolean createEmptyCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 95a8b775e1..e5a11ca82a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,10 +14,15 @@
package com.google.gerrit.server.project;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.StringUtil;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.util.TreeFormatter;
+import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -35,7 +40,10 @@ import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
@@ -51,6 +59,12 @@ public class ListProjects {
return !PERMISSIONS.matches(git);
}
},
+ PARENT_CANDIDATES {
+ @Override
+ boolean matches(Repository git) {
+ return true;
+ }
+ },
PERMISSIONS {
@Override
boolean matches(Repository git) throws IOException {
@@ -75,6 +89,9 @@ public class ListProjects {
private final GitRepositoryManager repoManager;
private final ProjectNode.Factory projectNodeFactory;
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
@Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
usage = "displays the sha of each project in the specified branch")
private List<String> showBranch;
@@ -93,6 +110,11 @@ public class ListProjects {
@Option(name = "--all", usage = "display all projects that are accessible by the calling user")
private boolean all;
+ @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
+ private int limit;
+
+ private String matchPrefix;
+
@Inject
protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
GitRepositoryManager repoManager,
@@ -115,6 +137,20 @@ public class ListProjects {
return showDescription;
}
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ public ListProjects setFormat(OutputFormat fmt) {
+ this.format = fmt;
+ return this;
+ }
+
+ public ListProjects setMatchPrefix(String prefix) {
+ this.matchPrefix = prefix;
+ return this;
+ }
+
public void display(OutputStream out) {
final PrintWriter stdout;
try {
@@ -124,88 +160,153 @@ public class ListProjects {
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
}
+ int found = 0;
+ Map<String, ProjectInfo> output = Maps.newTreeMap();
+ Map<String, String> hiddenNames = Maps.newHashMap();
+ Set<String> rejected = new HashSet<String>();
+
final TreeMap<Project.NameKey, ProjectNode> treeMap =
new TreeMap<Project.NameKey, ProjectNode>();
try {
- for (final Project.NameKey projectName : projectCache.all()) {
+ for (final Project.NameKey projectName : scan()) {
final ProjectState e = projectCache.get(projectName);
if (e == null) {
// If we can't get it from the cache, pretend its not present.
//
continue;
}
+ ProjectInfo info = new ProjectInfo();
+ if (type == FilterType.PARENT_CANDIDATES) {
+ ProjectState parentState = e.getParentState();
+ if (parentState != null
+ && !output.keySet().contains(parentState.getProject().getName())
+ && !rejected.contains(parentState.getProject().getName())) {
+ ProjectControl parentCtrl = parentState.controlFor(currentUser);
+ if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+ info.name = parentState.getProject().getName();
+ info.description = parentState.getProject().getDescription();
+ } else {
+ rejected.add(parentState.getProject().getName());
+ continue;
+ }
+ } else {
+ continue;
+ }
- final ProjectControl pctl = e.controlFor(currentUser);
- final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
- if (showTree) {
- treeMap.put(projectName,
- projectNodeFactory.create(pctl.getProject(), isVisible));
- continue;
- }
+ } else {
+ final ProjectControl pctl = e.controlFor(currentUser);
+ final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
+ if (showTree && !format.isJson()) {
+ treeMap.put(projectName,
+ projectNodeFactory.create(pctl.getProject(), isVisible));
+ continue;
+ }
- if (!isVisible) {
- // Require the project itself to be visible to the user.
- //
- continue;
- }
+ if (!isVisible && !(showTree && pctl.isOwner())) {
+ // Require the project itself to be visible to the user.
+ //
+ continue;
+ }
- try {
- if (showBranch != null) {
- Repository git = repoManager.openRepository(projectName);
- try {
- if (!type.matches(git)) {
- continue;
+ info.name = projectName.get();
+ if (showTree && format.isJson()) {
+ ProjectState parent = e.getParentState();
+ if (parent != null) {
+ ProjectControl parentCtrl = parent.controlFor(currentUser);
+ if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+ info.parent = parent.getProject().getName();
+ } else {
+ info.parent = hiddenNames.get(parent.getProject().getName());
+ if (info.parent == null) {
+ info.parent = "?-" + (hiddenNames.size() + 1);
+ hiddenNames.put(parent.getProject().getName(), info.parent);
+ }
}
+ }
+ }
+ if (showDescription && !e.getProject().getDescription().isEmpty()) {
+ info.description = e.getProject().getDescription();
+ }
- List<Ref> refs = getBranchRefs(projectName, pctl);
- if (!hasValidRef(refs)) {
- continue;
- }
+ try {
+ if (showBranch != null) {
+ Repository git = repoManager.openRepository(projectName);
+ try {
+ if (!type.matches(git)) {
+ continue;
+ }
- for (Ref ref : refs) {
- if (ref == null) {
- // Print stub (forty '-' symbols)
- stdout.print("----------------------------------------");
- } else {
- stdout.print(ref.getObjectId().name());
+ List<Ref> refs = getBranchRefs(projectName, pctl);
+ if (!hasValidRef(refs)) {
+ continue;
}
- stdout.print(' ');
- }
- } finally {
- git.close();
- }
- } else if (type != FilterType.ALL) {
- Repository git = repoManager.openRepository(projectName);
- try {
- if (!type.matches(git)) {
- continue;
+ for (int i = 0; i < showBranch.size(); i++) {
+ Ref ref = refs.get(i);
+ if (ref != null && ref.getObjectId() != null) {
+ if (info.branches == null) {
+ info.branches = Maps.newLinkedHashMap();
+ }
+ info.branches.put(showBranch.get(i), ref.getObjectId().name());
+ }
+ }
+ } finally {
+ git.close();
+ }
+ } else if (!showTree && type != FilterType.ALL) {
+ Repository git = repoManager.openRepository(projectName);
+ try {
+ if (!type.matches(git)) {
+ continue;
+ }
+ } finally {
+ git.close();
}
- } finally {
- git.close();
}
+
+ } catch (RepositoryNotFoundException err) {
+ // If the Git repository is gone, the project doesn't actually exist anymore.
+ continue;
+ } catch (IOException err) {
+ log.warn("Unexpected error reading " + projectName, err);
+ continue;
}
+ }
- } catch (RepositoryNotFoundException err) {
- // If the Git repository is gone, the project doesn't actually exist anymore.
- continue;
- } catch (IOException err) {
- log.warn("Unexpected error reading " + projectName, err);
+ if (limit > 0 && ++found > limit) {
+ break;
+ }
+
+ if (format.isJson()) {
+ output.put(info.name, info);
continue;
}
- stdout.print(projectName.get());
+ if (showBranch != null) {
+ for (String name : showBranch) {
+ String ref = info.branches != null ? info.branches.get(name) : null;
+ if (ref == null) {
+ // Print stub (forty '-' symbols)
+ ref = "----------------------------------------";
+ }
+ stdout.print(ref);
+ stdout.print(' ');
+ }
+ }
+ stdout.print(info.name);
- String desc;
- if (showDescription && !(desc = e.getProject().getDescription()).isEmpty()) {
+ if (info.description != null) {
// We still want to list every project as one-liners, hence escaping \n.
- stdout.print(" - " + desc.replace("\n", "\\n"));
+ stdout.print(" - " + StringUtil.escapeString(info.description));
}
-
- stdout.print("\n");
+ stdout.print('\n');
}
- if (showTree && treeMap.size() > 0) {
+ if (format.isJson()) {
+ format.newGson().toJson(
+ output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
+ stdout.print('\n');
+ } else if (showTree && treeMap.size() > 0) {
printProjectTree(stdout, treeMap);
}
} finally {
@@ -213,6 +314,14 @@ public class ListProjects {
}
}
+ private Iterable<NameKey> scan() {
+ if (matchPrefix != null) {
+ return projectCache.byName(matchPrefix);
+ } else {
+ return projectCache.all();
+ }
+ }
+
private void printProjectTree(final PrintWriter stdout,
final TreeMap<Project.NameKey, ProjectNode> treeMap) {
final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
@@ -270,4 +379,11 @@ public class ListProjects {
}
return false;
}
+
+ private static class ProjectInfo {
+ transient String name;
+ String parent;
+ String description;
+ Map<String, String> branches;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 70f40132d5..7a11131168 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -32,6 +32,12 @@ public interface ProjectCache {
/** Invalidate the cached information about the given project. */
public void evict(Project p);
+ /**
+ * Remove information about the given project from the cache. It will no
+ * longer be returned from {@link #all()}.
+ */
+ void remove(Project p);
+
/** @return sorted iteration of projects. */
public abstract Iterable<Project.NameKey> all();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 51d8fb2f60..a7fdb4e8b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,10 +14,11 @@
package com.google.gerrit.server.project;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
@@ -29,18 +30,23 @@ import com.google.inject.name.Named;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** Cache of project information, including access rights. */
@Singleton
public class ProjectCacheImpl implements ProjectCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectCacheImpl.class);
+
private static final String CACHE_NAME = "projects";
private static final String CACHE_LIST = "project_list";
@@ -48,13 +54,14 @@ public class ProjectCacheImpl implements ProjectCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<Project.NameKey, ProjectState>> nameType =
- new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
- core(nameType, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME, String.class, ProjectState.class)
+ .loader(Loader.class);
- final TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>> listType =
- new TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>>() {};
- core(listType, CACHE_LIST).populateWith(Lister.class);
+ cache(CACHE_LIST,
+ ListKey.class,
+ new TypeLiteral<SortedSet<Project.NameKey>>() {})
+ .maximumWeight(1)
+ .loader(Lister.class);
bind(ProjectCacheImpl.class);
bind(ProjectCache.class).to(ProjectCacheImpl.class);
@@ -63,16 +70,16 @@ public class ProjectCacheImpl implements ProjectCache {
}
private final AllProjectsName allProjectsName;
- private final Cache<Project.NameKey, ProjectState> byName;
- private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+ private final LoadingCache<String, ProjectState> byName;
+ private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
private final Lock listLock;
private final ProjectCacheClock clock;
@Inject
ProjectCacheImpl(
final AllProjectsName allProjectsName,
- @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
- @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+ @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+ @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
ProjectCacheClock clock) {
this.allProjectsName = allProjectsName;
this.byName = byName;
@@ -99,29 +106,55 @@ public class ProjectCacheImpl implements ProjectCache {
* @return the cached data; null if no such project exists.
*/
public ProjectState get(final Project.NameKey projectName) {
- ProjectState state = byName.get(projectName);
- if (state != null && state.needsRefresh(clock.read())) {
- byName.remove(projectName);
- state = byName.get(projectName);
+ if (projectName == null) {
+ return null;
+ }
+ try {
+ ProjectState state = byName.get(projectName.get());
+ if (state != null && state.needsRefresh(clock.read())) {
+ byName.invalidate(projectName.get());
+ state = byName.get(projectName.get());
+ }
+ return state;
+ } catch (ExecutionException e) {
+ if (!(e.getCause() instanceof RepositoryNotFoundException)) {
+ log.warn(String.format("Cannot read project %s", projectName.get()), e);
+ }
+ return null;
}
- return state;
}
/** Invalidate the cached information about the given project. */
public void evict(final Project p) {
if (p != null) {
- byName.remove(p.getNameKey());
+ byName.invalidate(p.getNameKey().get());
}
}
@Override
+ public void remove(final Project p) {
+ listLock.lock();
+ try {
+ SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
+ n.remove(p.getNameKey());
+ list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+ } catch (ExecutionException e) {
+ log.warn("Cannot list avaliable projects", e);
+ } finally {
+ listLock.unlock();
+ }
+ evict(p);
+ }
+
+ @Override
public void onCreateProject(Project.NameKey newProjectName) {
listLock.lock();
try {
- SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
- n = new TreeSet<Project.NameKey>(n);
+ SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
n.add(newProjectName);
list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+ } catch (ExecutionException e) {
+ log.warn("Cannot list avaliable projects", e);
} finally {
listLock.unlock();
}
@@ -129,18 +162,28 @@ public class ProjectCacheImpl implements ProjectCache {
@Override
public Iterable<Project.NameKey> all() {
- return list.get(ListKey.ALL);
+ try {
+ return list.get(ListKey.ALL);
+ } catch (ExecutionException e) {
+ log.warn("Cannot list available projects", e);
+ return Collections.emptyList();
+ }
}
@Override
public Iterable<Project.NameKey> byName(final String pfx) {
+ final Iterable<Project.NameKey> src;
+ try {
+ src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
+ } catch (ExecutionException e) {
+ return Collections.emptyList();
+ }
return new Iterable<Project.NameKey>() {
@Override
public Iterator<Project.NameKey> iterator() {
return new Iterator<Project.NameKey>() {
+ private Iterator<Project.NameKey> itr = src.iterator();
private Project.NameKey next;
- private Iterator<Project.NameKey> itr =
- list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
@Override
public boolean hasNext() {
@@ -182,7 +225,7 @@ public class ProjectCacheImpl implements ProjectCache {
};
}
- static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
+ static class Loader extends CacheLoader<String, ProjectState> {
private final ProjectState.Factory projectStateFactory;
private final GitRepositoryManager mgr;
@@ -193,19 +236,15 @@ public class ProjectCacheImpl implements ProjectCache {
}
@Override
- public ProjectState createEntry(Project.NameKey key) throws Exception {
+ public ProjectState load(String projectName) throws Exception {
+ Project.NameKey key = new Project.NameKey(projectName);
+ Repository git = mgr.openRepository(key);
try {
- Repository git = mgr.openRepository(key);
- try {
- final ProjectConfig cfg = new ProjectConfig(key);
- cfg.load(git);
- return projectStateFactory.create(cfg);
- } finally {
- git.close();
- }
-
- } catch (RepositoryNotFoundException notFound) {
- return null;
+ ProjectConfig cfg = new ProjectConfig(key);
+ cfg.load(git);
+ return projectStateFactory.create(cfg);
+ } finally {
+ git.close();
}
}
}
@@ -217,7 +256,7 @@ public class ProjectCacheImpl implements ProjectCache {
}
}
- static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+ static class Lister extends CacheLoader<ListKey, SortedSet<Project.NameKey>> {
private final GitRepositoryManager mgr;
@Inject
@@ -226,7 +265,7 @@ public class ProjectCacheImpl implements ProjectCache {
}
@Override
- public SortedSet<Project.NameKey> createEntry(ListKey key) throws Exception {
+ public SortedSet<Project.NameKey> load(ListKey key) throws Exception {
return mgr.list();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 29a6432456..513f1b1920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,37 +14,30 @@
package com.google.gerrit.server.project;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GitReceivePackGroups;
import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -56,9 +49,6 @@ import javax.annotation.Nullable;
/** Access control management for a user accessing a project's data. */
public class ProjectControl {
- private static final Logger log =
- LoggerFactory.getLogger(ProjectControl.class);
-
public static final int VISIBLE = 1 << 0;
public static final int OWNER = 1 << 1;
@@ -124,28 +114,26 @@ public class ProjectControl {
private final Set<AccountGroup.UUID> receiveGroups;
private final String canonicalWebUrl;
- private final SchemaFactory<ReviewDb> schema;
private final CurrentUser user;
private final ProjectState state;
- private final GroupCache groupCache;
private final PermissionCollection.Factory permissionFilter;
+ private final Collection<ContributorAgreement> contributorAgreements;
private List<SectionMatcher> allSections;
private Map<String, RefControl> refControls;
private Boolean declaredOwner;
+
@Inject
ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
@GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
- final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
- final PermissionCollection.Factory permissionFilter,
+ final ProjectCache pc, final PermissionCollection.Factory permissionFilter,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
@Assisted CurrentUser who, @Assisted ProjectState ps) {
this.uploadGroups = uploadGroups;
this.receiveGroups = receiveGroups;
- this.schema = schema;
- this.groupCache = groupCache;
this.permissionFilter = permissionFilter;
+ this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
this.canonicalWebUrl = canonicalWebUrl;
user = who;
state = ps;
@@ -198,7 +186,7 @@ public class ProjectControl {
/** Can this user see this project exists? */
public boolean isVisible() {
- return (visibleForReplication()
+ return (user instanceof InternalUser
|| canPerformOnAnyRef(Permission.READ)) && !isHidden();
}
@@ -209,14 +197,12 @@ public class ProjectControl {
/** Can this user see all the refs in this projects? */
public boolean allRefsAreVisible() {
- return visibleForReplication()
- || canPerformOnAllRefs(Permission.READ);
+ return allRefsAreVisibleExcept(Collections.<String> emptySet());
}
- /** Is this project completely visible for replication? */
- boolean visibleForReplication() {
- return user instanceof ReplicationUser
- && ((ReplicationUser) user).isEverythingVisible();
+ public boolean allRefsAreVisibleExcept(Set<String> except) {
+ return user instanceof InternalUser
+ || canPerformOnAllRefs(Permission.READ, except);
}
/** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -247,12 +233,7 @@ public class ProjectControl {
}
Project project = state.getProject();
if (project.isUseContributorAgreements()) {
- try {
- return verifyActiveContributorAgreement();
- } catch (OrmException e) {
- log.error("Cannot query database for agreements", e);
- return new Capable("Cannot verify contribution agreement");
- }
+ return verifyActiveContributorAgreement();
}
return Capable.OK;
}
@@ -270,119 +251,60 @@ public class ProjectControl {
return all;
}
- private Capable verifyActiveContributorAgreement() throws OrmException {
+ private Capable verifyActiveContributorAgreement() {
if (! (user instanceof IdentifiedUser)) {
return new Capable("Must be logged in to verify Contributor Agreement");
}
final IdentifiedUser iUser = (IdentifiedUser) user;
- final ReviewDb db = schema.open();
-
- AbstractAgreement bestAgreement = null;
- ContributorAgreement bestCla = null;
- try {
-
- OUTER: for (AccountGroup.UUID groupUUID : iUser.getEffectiveGroups().getKnownGroups()) {
- AccountGroup group = groupCache.get(groupUUID);
- if (group == null) {
- continue;
- }
- final List<AccountGroupAgreement> temp =
- db.accountGroupAgreements().byGroup(group.getId()).toList();
-
- Collections.reverse(temp);
-
- for (final AccountGroupAgreement a : temp) {
- final ContributorAgreement cla =
- db.contributorAgreements().get(a.getAgreementId());
- if (cla == null) {
- continue;
- }
-
- bestAgreement = a;
- bestCla = cla;
- break OUTER;
- }
+ boolean hasContactInfo = !missing(iUser.getAccount().getFullName())
+ && !missing(iUser.getAccount().getPreferredEmail())
+ && iUser.getAccount().isContactFiled();
+
+ List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
+ List<AccountGroup.UUID> missingInfoGroupIds = Lists.newArrayList();
+ for (ContributorAgreement ca : contributorAgreements) {
+ List<AccountGroup.UUID> groupIds;
+ if (hasContactInfo || !ca.isRequireContactInformation()) {
+ groupIds = okGroupIds;
+ } else {
+ groupIds = missingInfoGroupIds;
}
- if (bestAgreement == null) {
- final List<AccountAgreement> temp =
- db.accountAgreements().byAccount(iUser.getAccountId()).toList();
-
- Collections.reverse(temp);
-
- for (final AccountAgreement a : temp) {
- final ContributorAgreement cla =
- db.contributorAgreements().get(a.getAgreementId());
- if (cla == null) {
- continue;
- }
-
- bestAgreement = a;
- bestCla = cla;
- break;
+ for (PermissionRule rule : ca.getAccepted()) {
+ if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)
+ && (rule.getGroup().getUUID() != null)) {
+ groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
}
}
- } finally {
- db.close();
}
+ if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
+ return Capable.OK;
+ }
- if (bestCla != null && !bestCla.isActive()) {
+ if (iUser.getEffectiveGroups().containsAnyOf(missingInfoGroupIds)) {
final StringBuilder msg = new StringBuilder();
- msg.append(bestCla.getShortName());
- msg.append(" contributor agreement is expired.\n");
+ for (ContributorAgreement ca : contributorAgreements) {
+ if (ca.isRequireContactInformation()) {
+ msg.append(ca.getName());
+ break;
+ }
+ }
+ msg.append(" contributor agreement requires");
+ msg.append(" current contact information.\n");
if (canonicalWebUrl != null) {
- msg.append("\nPlease complete a new agreement");
+ msg.append("\nPlease review your contact information");
msg.append(":\n\n ");
msg.append(canonicalWebUrl);
msg.append("#");
- msg.append(PageLinks.SETTINGS_AGREEMENTS);
+ msg.append(PageLinks.SETTINGS_CONTACT);
msg.append("\n");
}
msg.append("\n");
return new Capable(msg.toString());
}
- if (bestCla != null && bestCla.isRequireContactInformation()) {
- boolean fail = false;
- fail |= missing(iUser.getAccount().getFullName());
- fail |= missing(iUser.getAccount().getPreferredEmail());
- fail |= !iUser.getAccount().isContactFiled();
-
- if (fail) {
- final StringBuilder msg = new StringBuilder();
- msg.append(bestCla.getShortName());
- msg.append(" contributor agreement requires");
- msg.append(" current contact information.\n");
- if (canonicalWebUrl != null) {
- msg.append("\nPlease review your contact information");
- msg.append(":\n\n ");
- msg.append(canonicalWebUrl);
- msg.append("#");
- msg.append(PageLinks.SETTINGS_CONTACT);
- msg.append("\n");
- }
- msg.append("\n");
- return new Capable(msg.toString());
- }
- }
-
- if (bestAgreement != null) {
- switch (bestAgreement.getStatus()) {
- case VERIFIED:
- return Capable.OK;
- case REJECTED:
- return new Capable(bestCla.getShortName()
- + " contributor agreement was rejected."
- + "\n (rejected on " + bestAgreement.getReviewedOn()
- + ")\n");
- case NEW:
- return new Capable(bestCla.getShortName()
- + " contributor agreement is still pending review.\n");
- }
- }
-
final StringBuilder msg = new StringBuilder();
msg.append(" A Contributor Agreement must be completed before uploading");
if (canonicalWebUrl != null) {
@@ -430,7 +352,7 @@ public class ProjectControl {
return false;
}
- private boolean canPerformOnAllRefs(String permission) {
+ private boolean canPerformOnAllRefs(String permission, Set<String> except) {
boolean canPerform = false;
Set<String> patterns = allRefPatterns(permission);
if (patterns.contains(AccessSection.ALL)) {
@@ -441,6 +363,8 @@ public class ProjectControl {
for (final String pattern : patterns) {
if (controlForRef(pattern).canPerform(permission)) {
canPerform = true;
+ } else if (except.contains(pattern)) {
+ continue;
} else {
return false;
}
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 6eac998af9..e06c948064 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,7 +14,7 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.common.CollectionsUtil;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
@@ -96,20 +96,24 @@ public class ProjectState {
? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
: null;
- HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
- AccessSection all = config.getAccessSection(AccessSection.ALL);
- if (all != null) {
- Permission owner = all.getPermission(Permission.OWNER);
- if (owner != null) {
- for (PermissionRule rule : owner.getRules()) {
- GroupReference ref = rule.getGroup();
- if (ref.getUUID() != null) {
- groups.add(ref.getUUID());
+ if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
+ localOwners = Collections.emptySet();
+ } else {
+ HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
+ AccessSection all = config.getAccessSection(AccessSection.ALL);
+ if (all != null) {
+ Permission owner = all.getPermission(Permission.OWNER);
+ if (owner != null) {
+ for (PermissionRule rule : owner.getRules()) {
+ GroupReference ref = rule.getGroup();
+ if (ref.getUUID() != null) {
+ groups.add(ref.getUUID());
+ }
}
}
}
+ localOwners = Collections.unmodifiableSet(groups);
}
- localOwners = Collections.unmodifiableSet(groups);
}
boolean needsRefresh(long generation) {
@@ -176,6 +180,18 @@ public class ProjectState {
Collection<AccessSection> fromConfig = config.getAccessSections();
sm = new ArrayList<SectionMatcher>(fromConfig.size());
for (AccessSection section : fromConfig) {
+ if (isAllProjects) {
+ List<Permission> copy =
+ Lists.newArrayListWithCapacity(section.getPermissions().size());
+ for (Permission p : section.getPermissions()) {
+ if (Permission.canBeOnAllProjects(section.getName(), p.getName())) {
+ copy.add(p);
+ }
+ }
+ section = new AccessSection(section.getName());
+ section.setPermissions(copy);
+ }
+
SectionMatcher matcher = SectionMatcher.wrap(section);
if (matcher != null) {
sm.add(matcher);
@@ -275,4 +291,8 @@ public class ProjectState {
}
return projectCache.get(getProject().getParent(allProjectsName));
}
+
+ public boolean isAllProjects() {
+ return isAllProjects;
+ }
} \ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index db370e0727..a6182d159e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -23,6 +23,7 @@ import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import dk.brics.automaton.RegExp;
@@ -101,7 +102,7 @@ public class RefControl {
/** Can this user see this reference exists? */
public boolean isVisible() {
- return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+ return (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
&& canRead();
}
@@ -154,7 +155,14 @@ public class RefControl {
// rules. Allowing this to be done by a non-project-owner opens
// a security hole enabling editing of access rules, and thus
// granting of powers beyond pushing to the configuration.
- return false;
+
+ // On the AllProjects project the owner access right cannot be assigned,
+ // this why for the AllProjects project we allow administrators to push
+ // configuration changes if they have push without being project owner.
+ if (!(projectControl.getProjectState().isAllProjects() &&
+ getCurrentUser().getCapabilities().canAdministrateServer())) {
+ return false;
+ }
}
return canPerform(Permission.PUSH)
&& canWrite();
@@ -309,6 +317,11 @@ public class RefControl {
return canPerform(Permission.FORGE_SERVER);
}
+ /** @return true if this user can abandon a change for this ref */
+ public boolean canAbandon() {
+ return canPerform(Permission.ABANDON);
+ }
+
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
List<PermissionRange> r = new ArrayList<PermissionRange>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 047997e23e..db879dea97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -14,14 +14,13 @@
package com.google.gerrit.server.project;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.util.MostSpecificComparator;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.slf4j.Logger;
@@ -44,9 +43,7 @@ public class SectionSortCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<EntryKey, EntryVal>> type =
- new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
- core(type, CACHE_NAME);
+ cache(CACHE_NAME, EntryKey.class, EntryVal.class);
bind(SectionSortCache.class);
}
};
@@ -66,7 +63,7 @@ public class SectionSortCache {
}
EntryKey key = new EntryKey(ref, sections);
- EntryVal val = cache.get(key);
+ EntryVal val = cache.getIfPresent(key);
if (val != null) {
int[] srcIdx = val.order;
if (srcIdx != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index ce47d2deed..b0f12b5587 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query;
+import com.google.common.collect.Iterables;
import com.google.gwtorm.server.OrmException;
import java.util.Collection;
@@ -42,25 +43,43 @@ import java.util.List;
* @type <T> type of object the predicate can evaluate in memory.
*/
public abstract class Predicate<T> {
+ /** A predicate that matches any input, always, with no cost. */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> any() {
+ return (Predicate<T>) Any.INSTANCE;
+ }
+
/** Combine the passed predicates into a single AND node. */
public static <T> Predicate<T> and(final Predicate<T>... that) {
+ if (that.length == 1) {
+ return that[0];
+ }
return new AndPredicate<T>(that);
}
/** Combine the passed predicates into a single AND node. */
public static <T> Predicate<T> and(
final Collection<? extends Predicate<T>> that) {
+ if (that.size() == 1) {
+ return Iterables.getOnlyElement(that);
+ }
return new AndPredicate<T>(that);
}
/** Combine the passed predicates into a single OR node. */
public static <T> Predicate<T> or(final Predicate<T>... that) {
+ if (that.length == 1) {
+ return that[0];
+ }
return new OrPredicate<T>(that);
}
/** Combine the passed predicates into a single OR node. */
public static <T> Predicate<T> or(
final Collection<? extends Predicate<T>> that) {
+ if (that.size() == 1) {
+ return Iterables.getOnlyElement(that);
+ }
return new OrPredicate<T>(that);
}
@@ -107,4 +126,36 @@ public abstract class Predicate<T> {
@Override
public abstract boolean equals(Object other);
+
+ private static class Any<T> extends Predicate<T> {
+ private static final Any<Object> INSTANCE = new Any<Object>();
+
+ private Any() {
+ }
+
+ @Override
+ public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+ return this;
+ }
+
+ @Override
+ public boolean match(T object) {
+ return true;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 7a85b6f0bd..d6762db368 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.TrackingId;
@@ -28,7 +31,10 @@ import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.ObjectId;
@@ -46,9 +52,61 @@ import java.util.List;
import java.util.Map;
public class ChangeData {
+ public static void ensureChangeLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ Map<Change.Id, ChangeData> missing = Maps.newHashMap();
+ for (ChangeData cd : changes) {
+ if (cd.change == null) {
+ missing.put(cd.getId(), cd);
+ }
+ }
+ if (!missing.isEmpty()) {
+ for (Change change : db.get().changes().get(missing.keySet())) {
+ missing.get(change.getId()).change = change;
+ }
+ }
+ }
+
+ public static void ensureCurrentPatchSetLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
+ for (ChangeData cd : changes) {
+ if (cd.currentPatchSet == null && cd.patches == null) {
+ missing.put(cd.change(db).currentPatchSetId(), cd);
+ }
+ }
+ if (!missing.isEmpty()) {
+ for (PatchSet ps : db.get().patchSets().get(missing.keySet())) {
+ ChangeData cd = missing.get(ps.getId());
+ cd.currentPatchSet = ps;
+ cd.patches = Lists.newArrayList(ps);
+ }
+ }
+ }
+
+ public static void ensureCurrentApprovalsLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
+ for (ChangeData cd : changes) {
+ if (cd.currentApprovals == null && cd.approvals == null) {
+ pending.add(db.get().patchSetApprovals()
+ .byPatchSet(cd.change(db).currentPatchSetId()));
+ }
+ }
+ if (!pending.isEmpty()) {
+ int idx = 0;
+ for (ChangeData cd : changes) {
+ if (cd.currentApprovals == null && cd.approvals == null) {
+ cd.currentApprovals = pending.get(idx++).toList();
+ }
+ }
+ }
+ }
+
private final Change.Id legacyId;
private Change change;
private String commitMessage;
+ private PatchSet currentPatchSet;
private Collection<PatchSet> patches;
private Collection<PatchSetApproval> approvals;
private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
@@ -57,6 +115,7 @@ public class ChangeData {
private Collection<PatchLineComment> comments;
private Collection<TrackingId> trackingIds;
private CurrentUser visibleTo;
+ private ChangeControl changeControl;
private List<ChangeMessage> messages;
public ChangeData(final Change.Id id) {
@@ -84,7 +143,14 @@ public class ChangeData {
return null;
}
- PatchList p = cache.get(c, ps);
+ PatchList p;
+ try {
+ p = cache.get(c, ps);
+ } catch (PatchListNotAvailableException e) {
+ currentFiles = new String[0];
+ return currentFiles;
+ }
+
List<String> r = new ArrayList<String>(p.getPatches().size());
for (PatchListEntry e : p.getPatches()) {
if (Patch.COMMIT_MSG.equals(e.getNewName())) {
@@ -125,8 +191,13 @@ public class ChangeData {
return visibleTo == user;
}
- void cacheVisibleTo(CurrentUser user) {
- visibleTo = user;
+ ChangeControl changeControl() {
+ return changeControl;
+ }
+
+ void cacheVisibleTo(ChangeControl ctl) {
+ visibleTo = ctl.getCurrentUser();
+ changeControl = ctl;
}
public Change change(Provider<ReviewDb> db) throws OrmException {
@@ -137,16 +208,19 @@ public class ChangeData {
}
public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
- Change c = change(db);
- if (c == null) {
- return null;
- }
- for (PatchSet p : patches(db)) {
- if (p.getId().equals(c.currentPatchSetId())) {
- return p;
+ if (currentPatchSet == null) {
+ Change c = change(db);
+ if (c == null) {
+ return null;
+ }
+ for (PatchSet p : patches(db)) {
+ if (p.getId().equals(c.currentPatchSetId())) {
+ currentPatchSet = p;
+ return p;
+ }
}
}
- return null;
+ return currentPatchSet;
}
public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
@@ -155,24 +229,21 @@ public class ChangeData {
Change c = change(db);
if (c == null) {
currentApprovals = Collections.emptyList();
+ } else if (approvals != null) {
+ Map<Id, Collection<PatchSetApproval>> map = approvalsMap(db);
+ currentApprovals = map.get(c.currentPatchSetId());
+ if (currentApprovals == null) {
+ currentApprovals = Collections.emptyList();
+ map.put(c.currentPatchSetId(), currentApprovals);
+ }
} else {
- currentApprovals = approvalsFor(db, c.currentPatchSetId());
+ currentApprovals = db.get().patchSetApprovals()
+ .byPatchSet(c.currentPatchSetId()).toList();
}
}
return currentApprovals;
}
- public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
- PatchSet.Id psId) throws OrmException {
- List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
- for (PatchSetApproval p : approvals(db)) {
- if (p.getPatchSetId().equals(psId)) {
- r.add(p);
- }
- }
- return r;
- }
-
public String commitMessage(GitRepositoryManager repoManager,
Provider<ReviewDb> db) throws IOException, OrmException {
if (commitMessage == null) {
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 4d8e806bbe..e80ad671e0 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
@@ -14,7 +14,9 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
@@ -25,7 +27,8 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListCache;
@@ -44,6 +47,7 @@ import org.eclipse.jgit.lib.AbbreviatedObjectId;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -67,6 +71,9 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
private static final Pattern PAT_LABEL =
Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
+ // NOTE: As new search operations are added, please keep the
+ // SearchSuggestOracle up to date.
+
public static final String FIELD_AGE = "age";
public static final String FIELD_BRANCH = "branch";
public static final String FIELD_CHANGE = "change";
@@ -103,7 +110,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
final ChangeControl.Factory changeControlFactory;
final ChangeControl.GenericFactory changeControlGenericFactory;
final AccountResolver accountResolver;
- final GroupCache groupCache;
+ final GroupBackend groupBackend;
final ApprovalTypes approvalTypes;
final AllProjectsName allProjectsName;
final PatchListCache patchListCache;
@@ -117,7 +124,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
CapabilityControl.Factory capabilityControlFactory,
ChangeControl.Factory changeControlFactory,
ChangeControl.GenericFactory changeControlGenericFactory,
- AccountResolver accountResolver, GroupCache groupCache,
+ AccountResolver accountResolver,
+ GroupBackend groupBackend,
ApprovalTypes approvalTypes,
AllProjectsName allProjectsName,
PatchListCache patchListCache,
@@ -130,7 +138,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
this.changeControlFactory = changeControlFactory;
this.changeControlGenericFactory = changeControlGenericFactory;
this.accountResolver = accountResolver;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.approvalTypes = approvalTypes;
this.allProjectsName = allProjectsName;
this.patchListCache = patchListCache;
@@ -206,10 +214,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
}
if ("draft".equalsIgnoreCase(value)) {
- if (currentUser instanceof IdentifiedUser) {
- return new HasDraftByPredicate(args.dbProvider,
- ((IdentifiedUser) currentUser).getAccountId());
- }
+ return new HasDraftByPredicate(args.dbProvider, self());
}
throw new IllegalArgumentException();
@@ -233,6 +238,14 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return new IsReviewedPredicate(args.dbProvider);
}
+ if ("owner".equalsIgnoreCase(value)) {
+ return new OwnerPredicate(args.dbProvider, self());
+ }
+
+ if ("reviewer".equalsIgnoreCase(value)) {
+ return new ReviewerPredicate(args.dbProvider, self());
+ }
+
try {
return status(value);
} catch (IllegalArgumentException e) {
@@ -303,58 +316,68 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@Operator
public Predicate<ChangeData> starredby(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ if ("self".equals(who)) {
+ return new IsStarredByPredicate(args.dbProvider, currentUser);
+ }
+ Set<Account.Id> m = parseAccount(who);
+ List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new IsStarredByPredicate(args.dbProvider,
+ args.userFactory.create(args.dbProvider, id)));
}
- return new IsStarredByPredicate(args.dbProvider, //
- args.userFactory.create(args.dbProvider, account.getId()));
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> watchedby(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ Set<Account.Id> m = parseAccount(who);
+ List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ if (currentUser instanceof IdentifiedUser
+ && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
+ p.add(new IsWatchedByPredicate(args, currentUser));
+ } else {
+ p.add(new IsWatchedByPredicate(args,
+ args.userFactory.create(args.dbProvider, id)));
+ }
}
- return new IsWatchedByPredicate(args, args.userFactory.create(
- args.dbProvider, account.getId()));
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> draftby(String who) throws QueryParseException,
OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ Set<Account.Id> m = parseAccount(who);
+ List<HasDraftByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new HasDraftByPredicate(args.dbProvider, id));
}
- return new HasDraftByPredicate(args.dbProvider, account.getId());
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account != null) {
- return visibleto(args.userFactory
- .create(args.dbProvider, account.getId()));
+ if ("self".equals(who)) {
+ return is_visible();
+ }
+ Set<Account.Id> m = args.accountResolver.findAll(who);
+ if (!m.isEmpty()) {
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ return visibleto(args.userFactory.create(args.dbProvider, id));
+ }
+ return Predicate.or(p);
}
// If its not an account, maybe its a group?
//
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
- if (g != null) {
- return visibleto(new SingleGroupUser(args.capabilityControlFactory,
- g.getGroupUUID()));
- }
-
- Collection<AccountGroup> matches =
- args.groupCache.get(new AccountGroup.ExternalNameKey(who));
- if (matches != null && !matches.isEmpty()) {
+ Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+ if (!suggestions.isEmpty()) {
HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
- for (AccountGroup group : matches) {
- ids.add(group.getGroupUUID());
+ for (GroupReference ref : suggestions) {
+ ids.add(ref.getUUID());
}
return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids));
}
@@ -375,57 +398,43 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@Operator
public Predicate<ChangeData> owner(String who) throws QueryParseException,
OrmException {
- Set<Account.Id> m = args.accountResolver.findAll(who);
- if (m.isEmpty()) {
- throw error("User " + who + " not found");
- } else if (m.size() == 1) {
- Account.Id id = m.iterator().next();
- return new OwnerPredicate(args.dbProvider, id);
- } else {
- List<OwnerPredicate> p = new ArrayList<OwnerPredicate>(m.size());
- for (Account.Id id : m) {
- p.add(new OwnerPredicate(args.dbProvider, id));
- }
- return Predicate.or(p);
+ Set<Account.Id> m = parseAccount(who);
+ List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new OwnerPredicate(args.dbProvider, id));
}
+ return Predicate.or(p);
}
@Operator
- public Predicate<ChangeData> ownerin(String group) throws QueryParseException,
- OrmException {
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+ public Predicate<ChangeData> ownerin(String group)
+ throws QueryParseException {
+ GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
throw error("Group " + group + " not found");
}
- return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+ return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
}
@Operator
public Predicate<ChangeData> reviewer(String who)
throws QueryParseException, OrmException {
- Set<Account.Id> m = args.accountResolver.findAll(who);
- if (m.isEmpty()) {
- throw error("User " + who + " not found");
- } else if (m.size() == 1) {
- Account.Id id = m.iterator().next();
- return new ReviewerPredicate(args.dbProvider, id);
- } else {
- List<ReviewerPredicate> p = new ArrayList<ReviewerPredicate>(m.size());
- for (Account.Id id : m) {
- p.add(new ReviewerPredicate(args.dbProvider, id));
- }
- return Predicate.or(p);
+ Set<Account.Id> m = parseAccount(who);
+ List<ReviewerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new ReviewerPredicate(args.dbProvider, id));
}
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> reviewerin(String group)
- throws QueryParseException, OrmException {
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+ throws QueryParseException {
+ GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
throw error("Group " + group + " not found");
}
- return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+ return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
}
@Operator
@@ -532,4 +541,23 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
throw error("Unsupported query:" + query);
}
}
+
+ private Set<Account.Id> parseAccount(String who)
+ throws QueryParseException, OrmException {
+ if ("self".equals(who)) {
+ return Collections.singleton(self());
+ }
+ Set<Account.Id> matches = args.accountResolver.findAll(who);
+ if (matches.isEmpty()) {
+ throw error("User " + who + " not found");
+ }
+ return matches;
+ }
+
+ private Account.Id self() {
+ if (currentUser instanceof IdentifiedUser) {
+ return ((IdentifiedUser) currentUser).getAccountId();
+ }
+ throw new IllegalArgumentException();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 413e6c4561..b73465a199 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -55,15 +55,18 @@ class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
}
try {
Change c = cd.change(db);
- if (c != null && changeControl.controlFor(c, user).isVisible(db.get())) {
- cd.cacheVisibleTo(user);
- return true;
- } else {
+ if (c == null) {
return false;
}
+
+ ChangeControl cc = changeControl.controlFor(c, user);
+ if (cc.isVisible(db.get())) {
+ cd.cacheVisibleTo(cc);
+ return true;
+ }
} catch (NoSuchChangeException e) {
- return false;
}
+ return false;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
new file mode 100644
index 0000000000..f0ed66a551
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
@@ -0,0 +1,672 @@
+// 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.query.change;
+
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
+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.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.jcraft.jsch.HostKey;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public class ListChanges {
+ private static final Logger log = LoggerFactory.getLogger(ListChanges.class);
+
+ @Singleton
+ static class Urls {
+ final String git;
+ final String http;
+
+ @Inject
+ Urls(@GerritServerConfig Config cfg) {
+ this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
+ this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
+ }
+
+ private static String ensureSlash(String in) {
+ if (in != null && !in.endsWith("/")) {
+ return in + "/";
+ }
+ return in;
+ }
+ }
+
+ private final QueryProcessor imp;
+ private final Provider<ReviewDb> db;
+ private final ApprovalTypes approvalTypes;
+ private final CurrentUser user;
+ private final AnonymousUser anonymous;
+ private final ChangeControl.Factory changeControlFactory;
+ private final PatchSetInfoFactory patchSetInfoFactory;
+ private final PatchListCache patchListCache;
+ private final SshInfo sshInfo;
+ private final Provider<String> urlProvider;
+ private final Urls urls;
+ private boolean reverse;
+ private Map<Account.Id, AccountAttribute> accounts;
+ private Map<Change.Id, ChangeControl> controls;
+ private EnumSet<ListChangesOption> options;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
+ private List<String> queries;
+
+ @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
+ public void setLimit(int limit) {
+ imp.setLimit(limit);
+ }
+
+ @Option(name = "-o", multiValued = true, usage = "Output options per change")
+ public void addOption(ListChangesOption o) {
+ options.add(o);
+ }
+
+ @Option(name = "-O", usage = "Output option flags, in hex")
+ void setOptionFlagsHex(String hex) {
+ options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+ }
+
+ @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
+ public void setSortKeyAfter(String key) {
+ // Querying for the prior page of changes requires sortkey_after predicate.
+ // Changes are shown most recent->least recent. The previous page of
+ // results contains changes that were updated after the given key.
+ imp.setSortkeyAfter(key);
+ reverse = true;
+ }
+
+ @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
+ public void setSortKeyBefore(String key) {
+ // Querying for the next page of changes requires sortkey_before predicate.
+ // Changes are shown most recent->least recent. The next page contains
+ // changes that were updated before the given key.
+ imp.setSortkeyBefore(key);
+ }
+
+ @Inject
+ ListChanges(QueryProcessor qp,
+ Provider<ReviewDb> db,
+ ApprovalTypes at,
+ CurrentUser u,
+ AnonymousUser au,
+ ChangeControl.Factory cf,
+ PatchSetInfoFactory psi,
+ PatchListCache plc,
+ SshInfo sshInfo,
+ @CanonicalWebUrl Provider<String> curl,
+ Urls urls) {
+ this.imp = qp;
+ this.db = db;
+ this.approvalTypes = at;
+ this.user = u;
+ this.anonymous = au;
+ this.changeControlFactory = cf;
+ this.patchSetInfoFactory = psi;
+ this.patchListCache = plc;
+ this.sshInfo = sshInfo;
+ this.urlProvider = curl;
+ this.urls = urls;
+
+ accounts = Maps.newHashMap();
+ controls = Maps.newHashMap();
+ options = EnumSet.noneOf(ListChangesOption.class);
+ }
+
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ public ListChanges setFormat(OutputFormat fmt) {
+ this.format = fmt;
+ return this;
+ }
+
+ public ListChanges addQuery(String query) {
+ if (queries == null) {
+ queries = Lists.newArrayList();
+ }
+ queries.add(query);
+ return this;
+ }
+
+ public void query(Writer out)
+ throws OrmException, QueryParseException, IOException {
+ if (imp.isDisabled()) {
+ throw new QueryParseException("query disabled");
+ }
+ if (queries == null || queries.isEmpty()) {
+ queries = Collections.singletonList("status:open");
+ } else if (queries.size() > 10) {
+ // Hard-code a default maximum number of queries to prevent
+ // users from submitting too much to the server in a single call.
+ throw new QueryParseException("limit of 10 queries");
+ }
+
+ List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
+ for (String query : queries) {
+ List<ChangeData> changes = imp.queryChanges(query);
+ boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
+ if (moreChanges) {
+ if (reverse) {
+ changes = changes.subList(1, changes.size());
+ } else {
+ changes = changes.subList(0, imp.getLimit());
+ }
+ }
+ ChangeData.ensureChangeLoaded(db, changes);
+ ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+ ChangeData.ensureCurrentApprovalsLoaded(db, changes);
+
+ List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
+ for (ChangeData cd : changes) {
+ info.add(toChangeInfo(cd));
+ }
+ if (moreChanges && !info.isEmpty()) {
+ if (reverse) {
+ info.get(0)._moreChanges = true;
+ } else {
+ info.get(info.size() - 1)._moreChanges = true;
+ }
+ }
+ res.add(info);
+ }
+
+ if (!accounts.isEmpty()) {
+ for (Account account : db.get().accounts().get(accounts.keySet())) {
+ AccountAttribute a = accounts.get(account.getId());
+ a.name = Strings.emptyToNull(account.getFullName());
+ }
+ }
+
+ if (format.isJson()) {
+ format.newGson().toJson(
+ res.size() == 1 ? res.get(0) : res,
+ new TypeToken<List<ChangeInfo>>() {}.getType(),
+ out);
+ out.write('\n');
+ } else {
+ boolean firstQuery = true;
+ for (List<ChangeInfo> info : res) {
+ if (firstQuery) {
+ firstQuery = false;
+ } else {
+ out.write('\n');
+ }
+ for (ChangeInfo c : info) {
+ String id = new Change.Key(c.id).abbreviate();
+ String subject = c.subject;
+ if (subject.length() + id.length() > 80) {
+ subject = subject.substring(0, 80 - id.length());
+ }
+ out.write(id);
+ out.write(' ');
+ out.write(subject.replace('\n', ' '));
+ out.write('\n');
+ }
+ }
+ }
+ }
+
+ private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
+ ChangeInfo out = new ChangeInfo();
+ Change in = cd.change(db);
+ out.project = in.getProject().get();
+ out.branch = in.getDest().getShortName();
+ out.topic = in.getTopic();
+ out.id = in.getKey().get();
+ out.subject = in.getSubject();
+ out.status = in.getStatus();
+ out.owner = asAccountAttribute(in.getOwner());
+ out.created = in.getCreatedOn();
+ out.updated = in.getLastUpdatedOn();
+ out._number = in.getId().get();
+ out._sortkey = in.getSortKey();
+ out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+ out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
+ out.labels = options.contains(LABELS) ? labelsFor(cd) : null;
+
+ if (options.contains(ALL_REVISIONS) || options.contains(CURRENT_REVISION)) {
+ out.revisions = revisions(cd);
+ for (String commit : out.revisions.keySet()) {
+ if (out.revisions.get(commit).isCurrent) {
+ out.current_revision = commit;
+ break;
+ }
+ }
+ }
+
+ return out;
+ }
+
+ private AccountAttribute asAccountAttribute(Account.Id user) {
+ if (user == null) {
+ return null;
+ }
+ AccountAttribute a = accounts.get(user);
+ if (a == null) {
+ a = new AccountAttribute();
+ accounts.put(user, a);
+ }
+ return a;
+ }
+
+ private ChangeControl control(ChangeData cd) throws OrmException {
+ ChangeControl ctrl = cd.changeControl();
+ if (ctrl != null && ctrl.getCurrentUser() == user) {
+ return ctrl;
+ }
+
+ ctrl = controls.get(cd.getId());
+ if (ctrl != null) {
+ return ctrl;
+ }
+
+ try {
+ ctrl = changeControlFactory.controlFor(cd.change(db));
+ } catch (NoSuchChangeException e) {
+ return null;
+ }
+ controls.put(cd.getId(), ctrl);
+ return ctrl;
+ }
+
+ private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
+ ChangeControl ctl = control(cd);
+ if (ctl == null) {
+ return Collections.emptyMap();
+ }
+
+ PatchSet ps = cd.currentPatchSet(db);
+ if (ps == null) {
+ return Collections.emptyMap();
+ }
+
+ Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
+ for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
+ if (rec.labels == null) {
+ continue;
+ }
+ for (SubmitRecord.Label r : rec.labels) {
+ LabelInfo p = labels.get(r.label);
+ if (p == null || p._status.compareTo(r.status) < 0) {
+ LabelInfo n = new LabelInfo();
+ n._status = r.status;
+ switch (r.status) {
+ case OK:
+ n.approved = asAccountAttribute(r.appliedBy);
+ break;
+ case REJECT:
+ n.rejected = asAccountAttribute(r.appliedBy);
+ break;
+ }
+ n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
+ labels.put(r.label, n);
+ }
+ }
+ }
+
+ Collection<PatchSetApproval> approvals = null;
+ for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+ if (e.getValue().approved != null || e.getValue().rejected != null) {
+ continue;
+ }
+
+ ApprovalType type = approvalTypes.byLabel(e.getKey());
+ if (type == null || type.getMin() == null || type.getMax() == null) {
+ // Unknown or misconfigured type can't have intermediate scores.
+ continue;
+ }
+
+ short min = type.getMin().getValue();
+ short max = type.getMax().getValue();
+ if (-1 <= min && max <= 1) {
+ // Types with a range of -1..+1 can't have intermediate scores.
+ continue;
+ }
+
+ if (approvals == null) {
+ approvals = cd.currentApprovals(db);
+ }
+ for (PatchSetApproval psa : approvals) {
+ short val = psa.getValue();
+ if (val != 0 && min < val && val < max
+ && psa.getCategoryId().equals(type.getCategory().getId())) {
+ if (0 < val) {
+ e.getValue().recommended = asAccountAttribute(psa.getAccountId());
+ e.getValue().value = val != 1 ? val : null;
+ } else {
+ e.getValue().disliked = asAccountAttribute(psa.getAccountId());
+ e.getValue().value = val != -1 ? val : null;
+ }
+ }
+ }
+ }
+ return labels;
+ }
+
+ private boolean isChangeReviewed(ChangeData cd) throws OrmException {
+ if (user instanceof IdentifiedUser) {
+ PatchSet currentPatchSet = cd.currentPatchSet(db);
+ if (currentPatchSet == null) {
+ return false;
+ }
+
+ List<ChangeMessage> messages =
+ db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
+
+ if (messages.isEmpty()) {
+ return false;
+ }
+
+ // Sort messages to let the most recent ones at the beginning.
+ Collections.sort(messages, new Comparator<ChangeMessage>() {
+ @Override
+ public int compare(ChangeMessage a, ChangeMessage b) {
+ return b.getWrittenOn().compareTo(a.getWrittenOn());
+ }
+ });
+
+ Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
+ Account.Id changeOwnerId = cd.change(db).getOwner();
+ for (ChangeMessage cm : messages) {
+ if (currentUserId.equals(cm.getAuthor())) {
+ return true;
+ } else if (changeOwnerId.equals(cm.getAuthor())) {
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ private Map<String, RevisionInfo> revisions(ChangeData cd) throws OrmException {
+ ChangeControl ctl = control(cd);
+ if (ctl == null) {
+ return Collections.emptyMap();
+ }
+
+ Collection<PatchSet> src;
+ if (options.contains(ALL_REVISIONS)) {
+ src = cd.patches(db);
+ } else {
+ src = Collections.singletonList(cd.currentPatchSet(db));
+ }
+ Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
+ for (PatchSet in : src) {
+ if (ctl.isPatchVisible(in, db.get())) {
+ res.put(in.getRevision().get(), toRevisionInfo(cd, in));
+ }
+ }
+ return res;
+ }
+
+ private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in)
+ throws OrmException {
+ RevisionInfo out = new RevisionInfo();
+ out.isCurrent = in.getId().equals(cd.change(db).currentPatchSetId());
+ out._number = in.getId().get();
+ out.draft = in.isDraft() ? true : null;
+ out.fetch = makeFetchMap(cd, in);
+
+ if (options.contains(ALL_COMMITS)
+ || (out.isCurrent && options.contains(CURRENT_COMMIT))) {
+ try {
+ PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
+ out.commit = new CommitInfo();
+ out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
+ out.commit.author = toGitPerson(info.getAuthor());
+ out.commit.committer = toGitPerson(info.getCommitter());
+ out.commit.subject = info.getSubject();
+ out.commit.message = info.getMessage();
+
+ for (ParentInfo parent : info.getParents()) {
+ CommitInfo i = new CommitInfo();
+ i.commit = parent.id.get();
+ i.subject = parent.shortMessage;
+ out.commit.parents.add(i);
+ }
+ } catch (PatchSetInfoNotAvailableException e) {
+ log.warn("Cannot load PatchSetInfo " + in.getId(), e);
+ }
+ }
+
+ if (options.contains(ALL_FILES)
+ || (out.isCurrent && options.contains(CURRENT_FILES))) {
+ PatchList list;
+ try {
+ list = patchListCache.get(cd.change(db), in);
+ } catch (PatchListNotAvailableException e) {
+ log.warn("Cannot load PatchList " + in.getId(), e);
+ list = null;
+ }
+ if (list != null) {
+ out.files = Maps.newTreeMap();
+ for (PatchListEntry e : list.getPatches()) {
+ if (Patch.COMMIT_MSG.equals(e.getNewName())) {
+ continue;
+ }
+
+ FileInfo d = new FileInfo();
+ d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
+ ? e.getChangeType().getCode()
+ : null;
+ d.oldPath = e.getOldName();
+ if (e.getPatchType() == Patch.PatchType.BINARY) {
+ d.binary = true;
+ } else {
+ d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
+ d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
+ }
+
+ FileInfo o = out.files.put(e.getNewName(), d);
+ if (o != null) {
+ // This should only happen on a delete-add break created by JGit
+ // when the file was rewritten and too little content survived. Write
+ // a single record with data from both sides.
+ d.status = Patch.ChangeType.REWRITE.getCode();
+ if (o.binary != null && o.binary) {
+ d.binary = true;
+ }
+ if (o.linesInserted != null) {
+ d.linesInserted = o.linesInserted;
+ }
+ if (o.linesDeleted != null) {
+ d.linesDeleted = o.linesDeleted;
+ }
+ }
+ }
+ }
+ }
+ return out;
+ }
+
+ private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+ throws OrmException {
+ Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+ String refName = in.getRefName();
+ ChangeControl ctl = control(cd);
+ if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
+ if (urls.git != null) {
+ r.put("git", new FetchInfo(urls.git
+ + cd.change(db).getProject().get(), refName));
+ }
+ }
+ if (urls.http != null) {
+ r.put("http", new FetchInfo(urls.http
+ + cd.change(db).getProject().get(), refName));
+ } else {
+ String http = urlProvider.get();
+ if (!Strings.isNullOrEmpty(http)) {
+ r.put("http", new FetchInfo(http
+ + cd.change(db).getProject().get(), refName));
+ }
+ }
+ if (!sshInfo.getHostKeys().isEmpty()) {
+ HostKey host = sshInfo.getHostKeys().get(0);
+ r.put("ssh", new FetchInfo(String.format(
+ "ssh://%s/%s",
+ host.getHost(), cd.change(db).getProject().get()),
+ refName));
+ }
+
+ return r;
+ }
+
+ private static GitPerson toGitPerson(UserIdentity committer) {
+ GitPerson p = new GitPerson();
+ p.name = committer.getName();
+ p.email = committer.getEmail();
+ p.date = committer.getDate();
+ p.tz = committer.getTimeZone();
+ return p;
+ }
+
+ static class ChangeInfo {
+ String project;
+ String branch;
+ String topic;
+ String id;
+ String subject;
+ Change.Status status;
+ Timestamp created;
+ Timestamp updated;
+ Boolean starred;
+ Boolean reviewed;
+
+ String _sortkey;
+ int _number;
+
+ AccountAttribute owner;
+ Map<String, LabelInfo> labels;
+ String current_revision;
+ Map<String, RevisionInfo> revisions;
+
+ Boolean _moreChanges;
+ }
+
+ static class RevisionInfo {
+ private transient boolean isCurrent;
+ Boolean draft;
+ int _number;
+ Map<String, FetchInfo> fetch;
+ CommitInfo commit;
+ Map<String, FileInfo> files;
+ }
+
+ static class FetchInfo {
+ String url;
+ String ref;
+
+ FetchInfo(String url, String ref) {
+ this.url = url;
+ this.ref = ref;
+ }
+ }
+
+ static class GitPerson {
+ String name;
+ String email;
+ Timestamp date;
+ int tz;
+ }
+
+ static class CommitInfo {
+ String commit;
+ List<CommitInfo> parents;
+ GitPerson author;
+ GitPerson committer;
+ String subject;
+ String message;
+ }
+
+ static class FileInfo {
+ Character status;
+ Boolean binary;
+ String oldPath;
+ Integer linesInserted;
+ Integer linesDeleted;
+ }
+
+ static class LabelInfo {
+ transient SubmitRecord.Label.Status _status;
+ AccountAttribute approved;
+ AccountAttribute rejected;
+
+ AccountAttribute recommended;
+ AccountAttribute disliked;
+ Short value;
+ Boolean optional;
+ }
+}
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 a2fa7fe34c..f44282a2f9 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
@@ -15,6 +15,7 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -55,6 +56,30 @@ public class QueryProcessor {
private static final Logger log =
LoggerFactory.getLogger(QueryProcessor.class);
+ private final Comparator<ChangeData> cmpAfter =
+ new Comparator<ChangeData>() {
+ @Override
+ public int compare(ChangeData a, ChangeData b) {
+ try {
+ return a.change(db).getSortKey().compareTo(b.change(db).getSortKey());
+ } catch (OrmException e) {
+ return 0;
+ }
+ }
+ };
+
+ private final Comparator<ChangeData> cmpBefore =
+ new Comparator<ChangeData>() {
+ @Override
+ public int compare(ChangeData a, ChangeData b) {
+ try {
+ return b.change(db).getSortKey().compareTo(a.change(db).getSortKey());
+ } catch (OrmException e) {
+ return 0;
+ }
+ }
+ };
+
public static enum OutputFormat {
TEXT, JSON;
}
@@ -71,6 +96,9 @@ public class QueryProcessor {
private final int maxLimit;
private OutputFormat outputFormat = OutputFormat.TEXT;
+ private int limit;
+ private String sortkeyAfter;
+ private String sortkeyBefore;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
@@ -78,6 +106,7 @@ public class QueryProcessor {
private boolean includeFiles;
private boolean includeCommitMessage;
private boolean includeDependencies;
+ private boolean includeSubmitRecords;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
@@ -97,6 +126,22 @@ public class QueryProcessor {
.getMax();
}
+ int getLimit() {
+ return limit;
+ }
+
+ void setLimit(int n) {
+ limit = n;
+ }
+
+ void setSortkeyAfter(String sortkey) {
+ sortkeyAfter = sortkey;
+ }
+
+ void setSortkeyBefore(String sortkey) {
+ sortkeyBefore = sortkey;
+ }
+
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
@@ -141,11 +186,23 @@ public class QueryProcessor {
includeCommitMessage = on;
}
+ public void setIncludeSubmitRecords(boolean on) {
+ includeSubmitRecords = on;
+ }
+
public void setOutput(OutputStream out, OutputFormat fmt) {
this.outputStream = out;
this.outputFormat = fmt;
}
+ /**
+ * Query for changes that match the query string.
+ * <p>
+ * If a limit was specified using {@link #setLimit(int)} this method may
+ * return up to {@code limit + 1} results, allowing the caller to determine if
+ * there are more than {@code limit} matches and suggest to its own caller
+ * that the query could be retried with {@link #setSortkeyBefore(String)}.
+ */
public List<ChangeData> queryChanges(final String queryString)
throws OrmException, QueryParseException {
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
@@ -175,19 +232,14 @@ public class QueryProcessor {
}
}
- Collections.sort(results, new Comparator<ChangeData>() {
- @Override
- public int compare(ChangeData a, ChangeData b) {
- return b.getChange().getSortKey().compareTo(
- a.getChange().getSortKey());
- }
- });
-
+ Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
int limit = limit(s);
if (limit < results.size()) {
results = results.subList(0, limit);
}
-
+ if (sortkeyAfter != null) {
+ Collections.reverse(results);
+ }
return results;
}
@@ -196,7 +248,7 @@ public class QueryProcessor {
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
- if (maxLimit <= 0) {
+ if (isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
@@ -213,6 +265,15 @@ public class QueryProcessor {
eventFactory.extend(c, d.getChange());
eventFactory.addTrackingIds(c, d.trackingIds(db));
+ if (includeSubmitRecords) {
+ PatchSet.Id psId = d.getChange().currentPatchSetId();
+ PatchSet patchSet = db.get().patchSets().get(psId);
+ Change.Id changeId = psId.getParentKey();
+ List<SubmitRecord> submitResult = d.changeControl().canSubmit( //
+ db.get(), patchSet, null, false, true);
+ eventFactory.addSubmitRecords(c, submitResult);
+ }
+
if (includeCommitMessage) {
eventFactory.addCommitMessage(c, d.commitMessage(repoManager, db));
}
@@ -233,7 +294,7 @@ public class QueryProcessor {
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet, //
- d.approvalsFor(db, current.getId()));
+ d.currentApprovals(db));
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
@@ -283,8 +344,13 @@ public class QueryProcessor {
}
}
+ boolean isDisabled() {
+ return maxLimit <= 0;
+ }
+
private int limit(Predicate<ChangeData> s) {
- return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+ int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+ return limit > 0 ? Math.min(n, limit) + 1 : n;
}
@SuppressWarnings("unchecked")
@@ -293,9 +359,17 @@ public class QueryProcessor {
Predicate<ChangeData> q = queryBuilder.parse(queryString);
if (!queryBuilder.hasSortKey(q)) {
- q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+ if (sortkeyBefore != null) {
+ q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
+ } else if (sortkeyAfter != null) {
+ q = Predicate.and(q, queryBuilder.sortkey_after(sortkeyAfter));
+ } else {
+ q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+ }
}
- q = Predicate.and(q, queryBuilder.limit(maxLimit), visibleToMe);
+ q = Predicate.and(q,
+ queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
+ visibleToMe);
Predicate<ChangeData> s = queryRewriter.rewrite(q);
if (!(s instanceof ChangeDataSource)) {
@@ -303,7 +377,7 @@ public class QueryProcessor {
}
if (!(s instanceof ChangeDataSource)) {
- throw new QueryParseException("cannot execute query: " + s);
+ throw new QueryParseException("invalid query: " + s);
}
return s;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index cdce217240..270b2e7d26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -27,15 +27,15 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Set;
-final class SingleGroupUser extends CurrentUser {
+public final class SingleGroupUser extends CurrentUser {
private final GroupMembership groups;
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
AccountGroup.UUID groupId) {
this(capabilityControlFactory, Collections.singleton(groupId));
}
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
Set<AccountGroup.UUID> groups) {
super(capabilityControlFactory, AccessPath.UNKNOWN);
this.groups = new ListGroupMembership(groups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 2a98e964da..cc480192ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -18,7 +18,7 @@ import static com.google.gerrit.server.config.ConfigUtil.getEnum;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
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 d8809da633..fd379b2440 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
@@ -32,9 +32,9 @@ 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.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcExecutor;
import com.google.gwtorm.jdbc.JdbcSchema;
@@ -161,7 +161,7 @@ public class SchemaCreator {
anonymous =
newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
anonymous.setDescription("Any user, signed-in or not");
- anonymous.setOwnerGroupId(admin.getId());
+ anonymous.setOwnerGroupUUID(admin.getGroupUUID());
anonymous.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(anonymous));
c.accountGroupNames().insert(
@@ -170,7 +170,7 @@ public class SchemaCreator {
registered =
newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
registered.setDescription("Any signed-in user");
- registered.setOwnerGroupId(admin.getId());
+ registered.setOwnerGroupUUID(admin.getGroupUUID());
registered.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(registered));
c.accountGroupNames().insert(
@@ -178,7 +178,7 @@ public class SchemaCreator {
final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
batchUsers.setDescription("Users who perform batch actions on Gerrit");
- batchUsers.setOwnerGroupId(admin.getId());
+ batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
batchUsers.setType(AccountGroup.Type.INTERNAL);
c.accountGroups().insert(Collections.singleton(batchUsers));
c.accountGroupNames().insert(
@@ -186,7 +186,7 @@ public class SchemaCreator {
owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
owners.setDescription("Any owner of the project");
- owners.setOwnerGroupId(admin.getId());
+ owners.setOwnerGroupUUID(admin.getGroupUUID());
owners.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(owners));
c.accountGroupNames().insert(
@@ -219,7 +219,8 @@ public class SchemaCreator {
}
}
try {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjectsName, git);
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
@@ -251,13 +252,12 @@ public class SchemaCreator {
all.getPermission(Permission.FORGE_AUTHOR, true) //
.add(rule(config, registered));
- meta.getPermission(Permission.READ, true) //
- .add(rule(config, owners));
+ Permission metaReadPermission = meta.getPermission(Permission.READ, true);
+ metaReadPermission.setExclusiveGroup(true);
+ metaReadPermission.add(rule(config, owners));
md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
- if (!config.commit(md)) {
- throw new IOException("Cannot create " + allProjectsName.get());
- }
+ config.commit(md);
} finally {
git.close();
}
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 f789300235..f127a2132e 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_64> C = Schema_64.class;
+ public static final Class<Schema_73> C = Schema_73.class;
public static class Module extends AbstractModule {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 75d8a39548..133b856b14 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.schema;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.server.ReviewDb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index d49b34e917..8207c31adc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -37,9 +37,9 @@ 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.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -158,13 +158,15 @@ class Schema_53 extends SchemaVersion {
// inheritable permissions. For example 'All-Projects'.
try {
git = mgr.createRepository(nameKey);
- } catch (RepositoryNotFoundException err) {
+ } catch (IOException err) {
throw new OrmException("Cannot create repository " + name, err);
}
+ } catch (IOException e) {
+ throw new OrmException(e);
}
try {
MetaDataUpdate md =
- new MetaDataUpdate(new NoReplication(), nameKey, git);
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, nameKey, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
@@ -182,9 +184,7 @@ class Schema_53 extends SchemaVersion {
}
md.setMessage("Import project configuration from SQL\n");
- if (!config.commit(md)) {
- throw new OrmException("Cannot export project " + name);
- }
+ config.commit(md);
} catch (ConfigInvalidException err) {
throw new OrmException("Cannot read project " + name, err);
} catch (IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 2247fdba5b..3a288e20d0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -26,9 +26,9 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -81,7 +81,8 @@ public class Schema_57 extends SchemaVersion {
try {
Repository git = mgr.openRepository(allProjects);
try {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjects, git);
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
@@ -134,9 +135,7 @@ public class Schema_57 extends SchemaVersion {
}
md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
- if (!config.commit(md)) {
- throw new OrmException("Cannot update " + allProjects);
- }
+ config.commit(md);
} finally {
git.close();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
index 26890a3a6b..e665bdca97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
@@ -23,9 +23,9 @@ import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
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.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,6 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -88,12 +87,12 @@ public class Schema_64 extends SchemaVersion {
Repository git;
try {
git = mgr.openRepository(allProjects);
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
throw new OrmException(e);
}
try {
MetaDataUpdate md =
- new MetaDataUpdate(new NoReplication(), allProjects, git);
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
@@ -107,9 +106,7 @@ public class Schema_64 extends SchemaVersion {
}
md.setMessage("Upgrade to Gerrit Code Review schema 64\n");
- if (!config.commit(md)) {
- throw new OrmException("Cannot update " + allProjects);
- }
+ config.commit(md);
} catch (IOException e) {
throw new OrmException(e);
} catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
new file mode 100644
index 0000000000..1cdf25cdea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -0,0 +1,461 @@
+// 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.schema;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Longs;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+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.AnonymousCowardName;
+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.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.SystemReader;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class Schema_65 extends SchemaVersion {
+ private final AllProjectsName allProjects;
+ private final GitRepositoryManager mgr;
+ private final PersonIdent serverUser;
+ private final @AnonymousCowardName String anonymousCowardName;
+
+ @Inject
+ Schema_65(Provider<Schema_64> prior,
+ AllProjectsName allProjects,
+ GitRepositoryManager mgr,
+ @GerritPersonIdent PersonIdent serverUser,
+ @AnonymousCowardName String anonymousCowardName) {
+ super(prior);
+ this.allProjects = allProjects;
+ this.mgr = mgr;
+ this.serverUser = serverUser;
+ this.anonymousCowardName = anonymousCowardName;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ Repository git;
+ try {
+ git = mgr.openRepository(allProjects);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ try {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+ ProjectConfig config = ProjectConfig.read(md);
+ Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
+ if (agreements.isEmpty()) {
+ return;
+ }
+ ui.message("Moved contributor agreements to project.config");
+
+ // Create the auto verify groups.
+ List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
+ for (ContributorAgreement agreement : agreements.values()) {
+ if (agreement.getAutoVerify() != null) {
+ getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+ }
+ }
+
+ // Scan AccountAgreement
+ long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
+
+ ProjectConfig base = ProjectConfig.read(md, null);
+ for (ContributorAgreement agreement : agreements.values()) {
+ base.replace(agreement);
+ }
+ base.getAccountsSection().setSameGroupVisibility(
+ config.getAccountsSection().getSameGroupVisibility());
+
+ BatchMetaDataUpdate batch = base.openUpdate(md);
+ try {
+ // Scan AccountGroupAgreement
+ List<AccountGroupAgreement> groupAgreements =
+ getAccountGroupAgreements(db, agreements);
+
+ // Find the earliest change
+ for (AccountGroupAgreement aga : groupAgreements) {
+ minTime = Math.min(minTime, aga.getTime());
+ }
+ minTime -= 60 * 1000; // 1 Minute
+
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(new PersonIdent(serverUser, new Date(minTime)));
+ commit.setCommitter(new PersonIdent(serverUser, new Date(minTime)));
+ commit.setMessage("Add the ContributorAgreements for upgrade to Gerrit Code Review schema 65\n");
+ batch.write(commit);
+
+ for (AccountGroupAgreement aga : groupAgreements) {
+ AccountGroup group = db.accountGroups().get(aga.groupId);
+ if (group == null) {
+ continue;
+ }
+
+ ContributorAgreement agreement = agreements.get(aga.claId);
+ agreement.getAccepted().add(new PermissionRule(config.resolve(group)));
+ base.replace(agreement);
+
+ PersonIdent ident = null;
+ if (aga.reviewedBy != null) {
+ Account ua = db.accounts().get(aga.reviewedBy);
+ if (ua != null) {
+ String name = ua.getFullName();
+ String email = ua.getPreferredEmail();
+
+ if (email == null || email.isEmpty()) {
+ // No preferred email is configured. Use a generic identity so we
+ // don't leak an address the user may have given us, but doesn't
+ // necessarily want to publish through Git records.
+ //
+ String user = ua.getUserName();
+ if (user == null || user.isEmpty()) {
+ user = "account-" + ua.getId().toString();
+ }
+
+ String host = SystemReader.getInstance().getHostname();
+ email = user + "@" + host;
+ }
+
+ if (name == null || name.isEmpty()) {
+ final int at = email.indexOf('@');
+ if (0 < at) {
+ name = email.substring(0, at);
+ } else {
+ name = anonymousCowardName;
+ }
+ }
+
+ ident = new PersonIdent(name, email, new Date(aga.getTime()), TimeZone.getDefault());
+ }
+ }
+ if (ident == null) {
+ ident = new PersonIdent(serverUser, new Date(aga.getTime()));
+ }
+
+ // Build the commits such that it keeps track of the date added and
+ // who added it.
+ commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(new PersonIdent(serverUser, new Date(aga.getTime())));
+
+ String msg = String.format("Accept %s contributor agreement for %s\n",
+ agreement.getName(), group.getName());
+ if (!Strings.isNullOrEmpty(aga.reviewComments)) {
+ msg += "\n" + aga.reviewComments + "\n";
+ }
+ commit.setMessage(msg);
+ batch.write(commit);
+ }
+
+ // Merge the agreements with the other data in project.config.
+ commit = new CommitBuilder();
+ commit.setAuthor(serverUser);
+ commit.setCommitter(serverUser);
+ commit.setMessage("Upgrade to Gerrit Code Review schema 65\n");
+ commit.addParentId(config.getRevision());
+ batch.write(config, commit);
+
+ // Save the the final metadata.
+ batch.commitAt(config.getRevision());
+ } finally {
+ batch.close();
+ }
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ } finally {
+ git.close();
+ }
+ }
+
+ private Map<Integer, ContributorAgreement> getAgreementToAdd(
+ ReviewDb db, ProjectConfig config) throws SQLException {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT short_name, id, require_contact_information," +
+ " short_description, agreement_url, auto_verify " +
+ "FROM contributor_agreements WHERE active = 'Y'");
+ try {
+ Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
+ while (rs.next()) {
+ String name = rs.getString(1);
+ if (config.getContributorAgreement(name) != null) {
+ continue; // already exists
+ }
+ ContributorAgreement a = config.getContributorAgreement(name, true);
+ agreements.put(rs.getInt(2), a);
+
+ a.setRequireContactInformation("Y".equals(rs.getString(3)));
+ a.setDescription(rs.getString(4));
+ a.setAgreementUrl(rs.getString(5));
+ if ("Y".equals(rs.getString(6))) {
+ a.setAutoVerify(new GroupReference(null, null));
+ }
+ }
+ return agreements;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private AccountGroup createGroup(ReviewDb db, String groupName,
+ AccountGroup.UUID adminGroupUUID, String description)
+ throws OrmException {
+ final AccountGroup.Id groupId =
+ new AccountGroup.Id(db.nextAccountGroupId());
+ final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
+ final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
+ final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
+ group.setOwnerGroupUUID(adminGroupUUID);
+ group.setDescription(description);
+ final AccountGroupName gn = new AccountGroupName(group);
+ // first insert the group name to validate that the group name hasn't
+ // already been used to create another group
+ db.accountGroupNames().insert(Collections.singleton(gn));
+ db.accountGroups().insert(Collections.singleton(group));
+ return group;
+ }
+
+ private List<AccountGroup.UUID> getAdministrateServerGroups(
+ ReviewDb db, ProjectConfig cfg) {
+ List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
+ .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
+ .getRules();
+
+ List<AccountGroup.UUID> groups =
+ Lists.newArrayListWithExpectedSize(rules.size());
+ for (PermissionRule rule : rules) {
+ if (rule.getAction() == Action.ALLOW) {
+ groups.add(rule.getGroup().getUUID());
+ }
+ }
+ if (groups.isEmpty()) {
+ throw new IllegalStateException("no administrator group found");
+ }
+
+ return groups;
+ }
+
+ private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
+ ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
+ ContributorAgreement agreement)
+ throws OrmException {
+ if (!agreement.getAccepted().isEmpty()) {
+ return agreement.getAccepted().get(0).getGroup();
+ }
+
+ String name = "CLA Accepted - " + agreement.getName();
+ AccountGroupName agn =
+ db.accountGroupNames().get(new AccountGroup.NameKey(name));
+ AccountGroup ag;
+ if (agn != null) {
+ ag = db.accountGroups().get(agn.getId());
+ if (ag == null) {
+ throw new IllegalStateException(
+ "account group name exists but account group does not: " + name);
+ }
+
+ if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
+ throw new IllegalStateException(
+ "individual group exists with non admin owner group: " + name);
+ }
+ } else {
+ ag = createGroup(db, name, adminGroupUUIDs.get(0),
+ String.format("Users who have accepted the %s CLA", agreement.getName()));
+ }
+ GroupReference group = config.resolve(ag);
+ agreement.setAccepted(Lists.newArrayList(new PermissionRule(group)));
+ if (agreement.getAutoVerify() != null) {
+ agreement.setAutoVerify(group);
+ }
+
+ // Don't allow accounts in the same individual CLA group to see each
+ // other in same group visibility mode.
+ List<PermissionRule> sameGroupVisibility =
+ config.getAccountsSection().getSameGroupVisibility();
+ PermissionRule rule = new PermissionRule(group);
+ rule.setDeny();
+ if (!sameGroupVisibility.contains(rule)) {
+ sameGroupVisibility.add(rule);
+ }
+ return group;
+ }
+
+ private long addAccountAgreements(ReviewDb db, ProjectConfig config,
+ List<AccountGroup.UUID> adminGroupUUIDs,
+ Map<Integer, ContributorAgreement> agreements)
+ throws SQLException, OrmException {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT account_id, cla_id, accepted_on, reviewed_by," +
+ " reviewed_on, review_comments " +
+ "FROM account_agreements WHERE status = 'V'");
+ try {
+ long minTime = System.currentTimeMillis();
+ while (rs.next()) {
+ Account.Id accountId = new Account.Id(rs.getInt(1));
+ Account.Id reviewerId = new Account.Id(rs.getInt(4));
+ if (rs.wasNull()) {
+ reviewerId = accountId;
+ }
+
+ int claId = rs.getInt(2);
+ ContributorAgreement agreement = agreements.get(claId);
+ if (agreement == null) {
+ continue; // Agreement is invalid
+ }
+
+ Timestamp acceptedOn = rs.getTimestamp(3);
+ minTime = Math.min(minTime, acceptedOn.getTime());
+
+ // Enter Agreement
+ GroupReference individualGroup =
+ getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+ AccountGroup.Id groupId = db.accountGroups()
+ .byUUID(individualGroup.getUUID())
+ .toList()
+ .get(0)
+ .getId();
+
+ final AccountGroupMember.Key key =
+ new AccountGroupMember.Key(accountId, groupId);
+ AccountGroupMember m = db.accountGroupMembers().get(key);
+ if (m == null) {
+ m = new AccountGroupMember(key);
+ db.accountGroupMembersAudit().insert(
+ Collections.singleton(
+ new AccountGroupMemberAudit(m, reviewerId, acceptedOn)));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ }
+ }
+ return minTime;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private static class AccountGroupAgreement {
+ private AccountGroup.Id groupId;
+ private int claId;
+ private Timestamp acceptedOn;
+ private Account.Id reviewedBy;
+ private Timestamp reviewedOn;
+ private String reviewComments;
+
+ private long getTime() {
+ return (reviewedOn == null) ? acceptedOn.getTime() : reviewedOn.getTime();
+ }
+ }
+
+ private List<AccountGroupAgreement> getAccountGroupAgreements(
+ ReviewDb db, Map<Integer, ContributorAgreement> agreements)
+ throws SQLException {
+
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, cla_id, accepted_on, reviewed_by, reviewed_on, " +
+ " review_comments " +
+ "FROM account_group_agreements");
+ try {
+ List<AccountGroupAgreement> groupAgreements = Lists.newArrayList();
+ while (rs.next()) {
+ AccountGroupAgreement a = new AccountGroupAgreement();
+ a.groupId = new AccountGroup.Id(rs.getInt(1));
+ a.claId = rs.getInt(2);
+ if (!agreements.containsKey(a.claId)) {
+ continue; // Agreement is invalid
+ }
+ a.acceptedOn = rs.getTimestamp(3);
+ a.reviewedBy = new Account.Id(rs.getInt(4));
+ if (rs.wasNull()) {
+ a.reviewedBy = null;
+ }
+
+ a.reviewedOn = rs.getTimestamp(5);
+ if (rs.wasNull()) {
+ a.reviewedOn = null;
+ }
+
+ a.reviewComments = rs.getString(6);
+ if (rs.wasNull()) {
+ a.reviewComments = null;
+ }
+ groupAgreements.add(a);
+ }
+ Collections.sort(groupAgreements, new Comparator<AccountGroupAgreement>() {
+ @Override
+ public int compare(
+ AccountGroupAgreement a1, AccountGroupAgreement a2) {
+ return Longs.compare(a1.getTime(), a2.getTime());
+ }
+ });
+ return groupAgreements;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
new file mode 100644
index 0000000000..94f5d2ce79
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
@@ -0,0 +1,46 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_66 extends SchemaVersion {
+
+ @Inject
+ Schema_66(Provider<Schema_65> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.executeUpdate("UPDATE accounts SET reverse_patch_set_order = 'Y' "+
+ "WHERE display_patch_sets_in_reverse_order = 'Y'");
+ stmt.executeUpdate("UPDATE accounts SET show_username_in_review_category = 'Y' " +
+ "WHERE display_person_name_in_review_category = 'Y'");
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
new file mode 100644
index 0000000000..bec2f3f498
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
@@ -0,0 +1,93 @@
+// 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.schema;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Schema_67 extends SchemaVersion {
+
+ @Inject
+ Schema_67(Provider<Schema_66> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ ui.message("Update ownerGroupId to ownerGroupUUID");
+
+ // Scan all AccountGroup, and find the ones that need the owner_group_id
+ // migrated to owner_group_uuid.
+ Map<AccountGroup.Id, AccountGroup.Id> idMap = Maps.newHashMap();
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, owner_group_id FROM account_groups"
+ + " WHERE owner_group_uuid is NULL or owner_group_uuid =''");
+ try {
+ while (rs.next()) {
+ AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+ AccountGroup.Id ownerId = new AccountGroup.Id(rs.getInt(2));
+ idMap.put(groupId, ownerId);
+ }
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+
+ // Lookup up all groups by ID.
+ Set<AccountGroup.Id> all =
+ Sets.newHashSet(Iterables.concat(idMap.keySet(), idMap.values()));
+ Map<AccountGroup.Id, AccountGroup> groups = Maps.newHashMap();
+ com.google.gwtorm.server.ResultSet<AccountGroup> rs =
+ db.accountGroups().get(all);
+ try {
+ for (AccountGroup group : rs) {
+ groups.put(group.getId(), group);
+ }
+ } finally {
+ rs.close();
+ }
+
+ // Update the ownerGroupUUID.
+ List<AccountGroup> toUpdate = Lists.newArrayListWithCapacity(idMap.size());
+ for (Entry<AccountGroup.Id, AccountGroup.Id> entry : idMap.entrySet()) {
+ AccountGroup group = groups.get(entry.getKey());
+ AccountGroup owner = groups.get(entry.getValue());
+ group.setOwnerGroupUUID(owner.getGroupUUID());
+ toUpdate.add(group);
+ }
+
+ db.accountGroups().update(toUpdate);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
new file mode 100644
index 0000000000..4dc2b6e4ba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
@@ -0,0 +1,60 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_68 extends SchemaVersion {
+ @Inject
+ Schema_68(Provider<Schema_67> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(final ReviewDb db, final UpdateUI ui)
+ throws SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.execute("CREATE INDEX submodule_subscription_access_bySubscription"
+ + " ON submodule_subscriptions (submodule_project_name, submodule_branch_name)");
+ } catch (SQLException e) {
+ // the index creation might have failed because the index exists already,
+ // in this case the exception can be safely ignored,
+ // but there are also other possible reasons for an exception here that
+ // should not be ignored,
+ // -> ask the user whether to ignore this exception or not
+ ui.message("warning: Cannot create index for submodule subscriptions");
+ ui.message(e.getMessage());
+
+ if (ui.isBatch()) {
+ ui.message("you may ignore this warning when running in interactive mode");
+ throw e;
+ } else {
+ final boolean answer = ui.yesno(false, "Ignore warning and proceed with schema upgrade");
+ if (!answer) {
+ throw e;
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
new file mode 100644
index 0000000000..fa56966f4a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
@@ -0,0 +1,228 @@
+// 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.schema;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+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.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.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+
+public class Schema_69 extends SchemaVersion {
+ private final GitRepositoryManager mgr;
+ private final PersonIdent serverUser;
+
+ @Inject
+ Schema_69(Provider<Schema_68> prior,
+ GitRepositoryManager mgr,
+ @GerritPersonIdent PersonIdent serverUser) {
+ super(prior);
+ this.mgr = mgr;
+ this.serverUser = serverUser;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+
+ // Find all groups that have an LDAP type.
+ Map<AccountGroup.UUID, GroupReference> ldapUUIDMap = Maps.newHashMap();
+ Set<AccountGroup.UUID> toResolve = Sets.newHashSet();
+ List<AccountGroup.Id> toDelete = Lists.newArrayList();
+ List<AccountGroup.NameKey> namesToDelete = Lists.newArrayList();
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, group_uuid, external_name, name FROM account_groups"
+ + " WHERE group_type ='LDAP'");
+ try {
+ while (rs.next()) {
+ AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+ AccountGroup.UUID groupUUID = new AccountGroup.UUID(rs.getString(2));
+ AccountGroup.NameKey name = new AccountGroup.NameKey(rs.getString(4));
+ String dn = rs.getString(3);
+
+ if (isNullOrEmpty(dn)) {
+ // The LDAP group does not have a DN. Determine if the UUID is used.
+ toResolve.add(groupUUID);
+ } else {
+ toDelete.add(groupId);
+ namesToDelete.add(name);
+ GroupReference ref = groupReference(dn);
+ ldapUUIDMap.put(groupUUID, ref);
+ }
+ }
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ if (toDelete.isEmpty() && toResolve.isEmpty()) {
+ return; // No ldap groups. Nothing to do.
+ }
+
+ ui.message("Update LDAP groups to be GroupReferences.");
+
+ // Update the groupOwnerUUID for LDAP groups to point to the new UUID.
+ List<AccountGroup> toUpdate = Lists.newArrayList();
+ Set<AccountGroup.UUID> resolveToUpdate = Sets.newHashSet();
+ Map<AccountGroup.UUID, AccountGroup> resolveGroups = Maps.newHashMap();
+ for (AccountGroup g : db.accountGroups().all()) {
+ if (ldapUUIDMap.containsKey(g.getGroupUUID())) {
+ continue; // Ignore the LDAP groups with a valid DN.
+ } else if (toResolve.contains(g.getGroupUUID())) {
+ resolveGroups.put(g.getGroupUUID(), g); // Keep the ones to resolve.
+ continue;
+ }
+
+ GroupReference ref = ldapUUIDMap.get(g.getOwnerGroupUUID());
+ if (ref != null) {
+ // Update the owner group UUID to the new ldap UUID scheme.
+ g.setOwnerGroupUUID(ref.getUUID());
+ toUpdate.add(g);
+ } else if (toResolve.contains(g.getOwnerGroupUUID())) {
+ // The unresolved group is used as an owner.
+ // Add to the list of LDAP groups to be made INTERNAL.
+ resolveToUpdate.add(g.getOwnerGroupUUID());
+ }
+ }
+
+ toResolve.removeAll(resolveToUpdate);
+
+ // Update project.config group references to use the new LDAP GroupReference
+ for (Project.NameKey name : mgr.list()) {
+ Repository git;
+ try {
+ git = mgr.openRepository(name);
+ } catch (RepositoryNotFoundException e) {
+ throw new OrmException(e);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+
+ try {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, name, git);
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+
+ ProjectConfig config = ProjectConfig.read(md);
+
+ // Update the existing refences to the new reference.
+ boolean updated = false;
+ for (Map.Entry<AccountGroup.UUID, GroupReference> entry: ldapUUIDMap.entrySet()) {
+ GroupReference ref = config.getGroup(entry.getKey());
+ if (ref != null) {
+ updated = true;
+ ref.setName(entry.getValue().getName());
+ ref.setUUID(entry.getValue().getUUID());
+ config.resolve(ref);
+ }
+ }
+
+ // Determine if a toResolve group is used and should be made INTERNAL.
+ Iterator<AccountGroup.UUID> iter = toResolve.iterator();
+ while (iter.hasNext()) {
+ AccountGroup.UUID uuid = iter.next();
+ if (config.getGroup(uuid) != null) {
+ resolveToUpdate.add(uuid);
+ iter.remove();
+ }
+ }
+
+ if (!updated) {
+ continue;
+ }
+
+ md.setMessage("Switch LDAP group UUIDs to DNs\n");
+ config.commit(md);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ } finally {
+ git.close();
+ }
+ }
+
+ for (AccountGroup.UUID uuid : resolveToUpdate) {
+ AccountGroup group = resolveGroups.get(uuid);
+ group.setType(AccountGroup.Type.INTERNAL);
+ toUpdate.add(group);
+
+ ui.message(String.format(
+ "*** Group has no DN and is inuse. Updated to be INTERNAL: %s",
+ group.getName()));
+ }
+
+ for (AccountGroup.UUID uuid : toResolve) {
+ AccountGroup group = resolveGroups.get(uuid);
+ toDelete.add(group.getId());
+ namesToDelete.add(group.getNameKey());
+ }
+
+ // Update group owners
+ db.accountGroups().update(toUpdate);
+ // Delete existing LDAP groups
+ db.accountGroupNames().deleteKeys(namesToDelete);
+ db.accountGroups().deleteKeys(toDelete);
+ }
+
+ private static GroupReference groupReference(String dn)
+ throws NamingException {
+ LdapName name = new LdapName(dn);
+ Preconditions.checkState(!name.isEmpty(), "Invalid LDAP dn: %s", dn);
+ String cn = name.get(name.size() - 1);
+ int index = cn.indexOf('=');
+ if (index >= 0) {
+ cn = cn.substring(index + 1);
+ }
+ return new GroupReference(new AccountGroup.UUID("ldap:" + dn), "ldap/" + cn);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
new file mode 100644
index 0000000000..03b33a07b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
@@ -0,0 +1,61 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_70 extends SchemaVersion {
+ @Inject
+ protected Schema_70(Provider<Schema_69> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+ SQLException {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.executeUpdate("UPDATE tracking_ids SET tracking_key = tracking_id");
+ execute(stmt, "DROP INDEX tracking_ids_byTrkId");
+ if (((JdbcSchema) db).getDialect() instanceof DialectPostgreSQL) {
+ execute(stmt, "ALTER TABLE tracking_ids DROP CONSTRAINT tracking_ids_pkey");
+ } else {
+ execute(stmt, "ALTER TABLE tracking_ids DROP PRIMARY KEY");
+ }
+ stmt.execute("ALTER TABLE tracking_ids"
+ + " ADD PRIMARY KEY (change_id, tracking_key, tracking_system)");
+ stmt.execute("CREATE INDEX tracking_ids_byTrkKey"
+ + " ON tracking_ids (tracking_key)");
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private static final void execute(Statement stmt, String command) {
+ try {
+ stmt.execute(command);
+ } catch (SQLException e) {
+ // ignore
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
new file mode 100644
index 0000000000..8d5b9438ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
@@ -0,0 +1,43 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+
+public class Schema_71 extends SchemaVersion {
+ @Inject
+ Schema_71(Provider<Schema_70> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(final ReviewDb db, final UpdateUI ui)
+ throws SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.executeUpdate("UPDATE account_diff_preferences SET show_line_endings='Y'");
+ }
+ finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java
new file mode 100644
index 0000000000..748837b985
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java
@@ -0,0 +1,25 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_72 extends SchemaVersion {
+ @Inject
+ Schema_72(Provider<Schema_71> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
new file mode 100644
index 0000000000..2732a3d8bd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
@@ -0,0 +1,43 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+
+public class Schema_73 extends SchemaVersion {
+ @Inject
+ Schema_73(Provider<Schema_72> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(final ReviewDb db, final UpdateUI ui)
+ throws SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.executeUpdate("CREATE INDEX change_messages_byPatchset ON change_messages (patchset_change_id, patchset_patch_set_id )");
+ }
+ finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index 64b3afaf60..eff55754fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -24,6 +24,8 @@ public interface UpdateUI {
boolean yesno(boolean def, String msg);
+ boolean isBatch();
+
void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
new file mode 100644
index 0000000000..686a108eae
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -0,0 +1,40 @@
+// 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.util;
+
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The default RequestContext to use when not in a request scope e.g.
+ * ThreadLocalRequestContext is not set.
+ */
+@Singleton
+public class FallbackRequestContext implements RequestContext {
+
+ private final AnonymousUser user;
+
+ @Inject
+ FallbackRequestContext(AnonymousUser user) {
+ this.user = user;
+ }
+
+ @Override
+ public CurrentUser getCurrentUser() {
+ return user;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 9befc7d190..fa07176166 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.util;
import com.google.common.collect.Maps;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.inject.Inject;
@@ -38,17 +37,15 @@ public class GuiceRequestScopePropagator extends RequestScopePropagator {
private final String url;
private final SocketAddress peer;
- private final CurrentUser user;
@Inject
GuiceRequestScopePropagator(
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
@RemotePeer Provider<SocketAddress> remotePeerProvider,
- Provider<CurrentUser> currentUserProvider) {
- super(ServletScopes.REQUEST);
+ ThreadLocalRequestContext local) {
+ super(ServletScopes.REQUEST, local);
this.url = urlProvider != null ? urlProvider.get() : null;
this.peer = remotePeerProvider.get();
- this.user = currentUserProvider.get();
}
/**
@@ -69,9 +66,6 @@ public class GuiceRequestScopePropagator extends RequestScopePropagator {
Providers.of(peer));
seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
- seedMap.put(Key.get(typeOfProvider(CurrentUser.class)), Providers.of(user));
- seedMap.put(Key.get(CurrentUser.class), user);
-
return ServletScopes.continueRequest(callable, seedMap);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
new file mode 100644
index 0000000000..ca8573f764
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
@@ -0,0 +1,26 @@
+// 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.util;
+
+import com.google.gerrit.server.CurrentUser;
+
+/**
+ * The RequestContext is an interface exposing the fields that are needed
+ * by the GerritGlobalModule scope.
+ */
+public interface RequestContext {
+
+ CurrentUser getCurrentUser();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 3661aa25d7..84c61e946b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -42,9 +42,12 @@ import java.util.concurrent.Executors;
public abstract class RequestScopePropagator {
private final Scope scope;
+ private final ThreadLocalRequestContext local;
- protected RequestScopePropagator(Scope scope) {
+ protected RequestScopePropagator(Scope scope,
+ ThreadLocalRequestContext local) {
this.scope = scope;
+ this.local = local;
}
/**
@@ -70,26 +73,8 @@ public abstract class RequestScopePropagator {
* @return a new Callable which will execute in the current request scope.
*/
public final <T> Callable<T> wrap(final Callable<T> callable) {
- final Callable<T> wrapped = wrapImpl(new Callable<T>() {
- @Override
- public T call() throws Exception {
- RequestCleanup cleanup = scope.scope(
- Key.get(RequestCleanup.class),
- new Provider<RequestCleanup>() {
- @Override
- public RequestCleanup get() {
- return new RequestCleanup();
- }
- }).get();
-
- try {
- return callable.call();
- } finally {
- cleanup.run();
- }
- }
- });
-
+ final Callable<T> wrapped =
+ wrapImpl(context(local.getContext(), cleanup(callable)));
return new Callable<T>() {
@Override
public T call() throws Exception {
@@ -178,4 +163,41 @@ public abstract class RequestScopePropagator {
* @see #wrap(Callable)
*/
protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
+
+ protected <T> Callable<T> context(final RequestContext context,
+ final Callable<T> callable) {
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ RequestContext old = local.setContext(context);
+ try {
+ return callable.call();
+ } finally {
+ local.setContext(old);
+ }
+ }
+ };
+ }
+
+ protected <T> Callable<T> cleanup(final Callable<T> callable) {
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ RequestCleanup cleanup = scope.scope(
+ Key.get(RequestCleanup.class),
+ new Provider<RequestCleanup>() {
+ @Override
+ public RequestCleanup get() {
+ return new RequestCleanup();
+ }
+ }).get();
+
+ try {
+ return callable.call();
+ } finally {
+ cleanup.run();
+ }
+ }
+ };
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
new file mode 100644
index 0000000000..b411512167
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -0,0 +1,87 @@
+// 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.util;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import javax.annotation.Nullable;
+
+/**
+ * ThreadLocalRequestContext manages the current RequestContext using a
+ * ThreadLocal. When the context is set, the fields exposed by the context
+ * are considered in scope. Otherwise, the FallbackRequestContext is used.
+ */
+public class ThreadLocalRequestContext {
+ private static final String FALLBACK = "FALLBACK";
+
+ public static Module module() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ThreadLocalRequestContext.class);
+ bind(RequestContext.class).annotatedWith(Names.named(FALLBACK))
+ .to(FallbackRequestContext.class);
+ }
+
+ @Provides
+ RequestContext provideRequestContext(
+ @Named(FALLBACK) RequestContext fallback) {
+ return Objects.firstNonNull(local.get(), fallback);
+ }
+
+ @Provides
+ CurrentUser provideCurrentUser(RequestContext ctx) {
+ return ctx.getCurrentUser();
+ }
+
+ @Provides
+ IdentifiedUser provideCurrentUser(CurrentUser user) {
+ if (user instanceof IdentifiedUser) {
+ return (IdentifiedUser) user;
+ }
+ throw new ProvisionException(NotSignedInException.MESSAGE,
+ new NotSignedInException());
+ }
+ };
+ }
+
+ private static final ThreadLocal<RequestContext> local =
+ new ThreadLocal<RequestContext>();
+
+ @Inject
+ ThreadLocalRequestContext() {
+ }
+
+ public RequestContext setContext(@Nullable RequestContext ctx) {
+ RequestContext old = getContext();
+ local.set(ctx);
+ return old;
+ }
+
+ @Nullable
+ public RequestContext getContext() {
+ return local.get();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 581ccc1483..7728d6fb5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -31,8 +31,8 @@ public abstract class ThreadLocalRequestScopePropagator<C>
private final ThreadLocal<C> threadLocal;
protected ThreadLocalRequestScopePropagator(Scope scope,
- ThreadLocal<C> threadLocal) {
- super(scope);
+ ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
+ super(scope, local);
this.threadLocal = threadLocal;
}
@@ -45,19 +45,16 @@ public abstract class ThreadLocalRequestScopePropagator<C>
return new Callable<T>() {
@Override
public T call() throws Exception {
- if (threadLocal.get() != null) {
- // This is consistent with the Guice ServletScopes.continueRequest()
- // behavior.
- throw new IllegalStateException("Cannot continue request, "
- + "thread already has request in progress. A new thread must "
- + "be used to propagate the request scope context.");
- }
-
+ C old = threadLocal.get();
threadLocal.set(ctx);
try {
return callable.call();
} finally {
- threadLocal.remove();
+ if (old != null) {
+ threadLocal.set(old);
+ } else {
+ threadLocal.remove();
+ }
}
}
};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index d08bd1fa08..74c97f3790 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -26,7 +26,6 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -57,7 +56,7 @@ public class FunctionState {
@Inject
FunctionState(final ApprovalTypes approvalTypes,
- final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
+ final IdentifiedUser.GenericFactory userFactory,
@Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
@Assisted final Collection<PatchSetApproval> all) {
this.approvalTypes = approvalTypes;
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index ac74147705..606e8831b4 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -27,7 +27,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
abstract class AbstractCommitUserIdentityPredicate extends Predicate.P3 {
- private static final long serialVersionUID = 1L;
private static final SymbolTerm user = SymbolTerm.intern("user", 1);
private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index c760426866..a26a4921fe 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -9,7 +9,9 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.util.Providers;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.JavaException;
@@ -24,8 +26,6 @@ import com.googlecode.prolog_cafe.lang.Term;
/** Exports list of {@code commit_label( label('Code-Review', 2), user(12345789) )}. */
class PRED__load_commit_labels_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
private static final SymbolTerm sym_commit_label = SymbolTerm.intern("commit_label", 2);
private static final SymbolTerm sym_label = SymbolTerm.intern("label", 2);
private static final SymbolTerm sym_user = SymbolTerm.intern("user", 1);
@@ -44,10 +44,18 @@ class PRED__load_commit_labels_1 extends Predicate.P1 {
try {
PrologEnvironment env = (PrologEnvironment) engine.control;
ReviewDb db = StoredValues.REVIEW_DB.get(engine);
- PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+ PatchSet patchSet = StoredValues.PATCH_SET.get(engine);
+ ChangeData cd = StoredValues.CHANGE_DATA.getOrNull(engine);
ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
- for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(patchSetId)) {
+ Iterable<PatchSetApproval> approvals;
+ if (cd != null) {
+ approvals = cd.currentApprovals(Providers.of(db));
+ } else {
+ approvals = db.patchSetApprovals().byPatchSet(patchSet.getId());
+ }
+
+ for (PatchSetApproval a : approvals) {
if (a.getValue() == 0) {
continue;
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index 68f9bf64da..a955307b6b 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -38,8 +38,6 @@ import com.googlecode.prolog_cafe.lang.Term;
* </pre>
*/
class PRED__user_label_range_4 extends Predicate.P4 {
- private static final long serialVersionUID = 1L;
-
PRED__user_label_range_4(Term a1, Term a2, Term a3, Term a4, Operation n) {
arg1 = a1;
arg2 = a2;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
index 7cc9f35fa9..51396ed9c4 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
@@ -26,8 +26,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_change_branch_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
public PRED_change_branch_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
index 09be90257c..b127fff254 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
@@ -28,7 +28,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_change_owner_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
private static final SymbolTerm user = SymbolTerm.intern("user", 1);
public PRED_change_owner_1(Term a1, Operation n) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
index d1cd20ddf6..fb9f86533a 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
@@ -26,8 +26,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_change_project_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
public PRED_change_project_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
index d200f7eec0..34885f9141 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
@@ -25,8 +25,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_change_topic_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
public PRED_change_topic_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
index a0817a1a78..370090915d 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
@@ -24,8 +24,6 @@ import com.googlecode.prolog_cafe.lang.PrologException;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_commit_author_3 extends AbstractCommitUserIdentityPredicate {
- private static final long serialVersionUID = 1L;
-
public PRED_commit_author_3(Term a1, Term a2, Term a3, Operation n) {
super(a1, a2, a3, n);
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
index 78e6cf72a5..64823dff38 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
@@ -24,8 +24,6 @@ import com.googlecode.prolog_cafe.lang.PrologException;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_commit_committer_3 extends AbstractCommitUserIdentityPredicate {
- private static final long serialVersionUID = 1L;
-
public PRED_commit_committer_3(Term a1, Term a2, Term a3, Operation n) {
super(a1, a2, a3, n);
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
index c2c2d1ca75..9ce7098240 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -43,7 +43,6 @@ import java.util.regex.Pattern;
* </pre>
*/
public class PRED_commit_delta_4 extends Predicate.P4 {
- private static final long serialVersionUID = 1L;
private static final SymbolTerm add = SymbolTerm.intern("add");
private static final SymbolTerm modify = SymbolTerm.intern("modify");
private static final SymbolTerm delete = SymbolTerm.intern("delete");
@@ -110,8 +109,8 @@ public class PRED_commit_delta_4 extends Predicate.P4 {
continue;
}
- if (regex.matcher(newName).matches() ||
- (oldName != null && regex.matcher(oldName).matches())) {
+ if (regex.matcher(newName).find() ||
+ (oldName != null && regex.matcher(oldName).find())) {
SymbolTerm changeSym = getTypeSymbol(changeType);
SymbolTerm newSym = SymbolTerm.create(newName);
SymbolTerm oldSym = Prolog.Nil;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
index f0accf0cbd..2c7949c191 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -54,8 +54,6 @@ import java.util.regex.Pattern;
* </pre>
*/
public class PRED_commit_edits_2 extends Predicate.P2 {
- private static final long serialVersionUID = 1L;
-
public PRED_commit_edits_2(Term a1, Term a2, Operation n) {
arg1 = a1;
arg2 = a2;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
index 0ed744325a..e2eb6b1301 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
@@ -32,8 +32,6 @@ import com.googlecode.prolog_cafe.lang.Term;
* </pre>
*/
public class PRED_commit_message_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
public PRED_commit_message_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 23cedcea0a..09a46f7592 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -20,7 +20,6 @@ import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.ReplicationUser;
import com.google.gerrit.server.project.ChangeControl;
import com.googlecode.prolog_cafe.lang.EvaluationException;
@@ -34,7 +33,6 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
public class PRED_current_user_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
private static final SymbolTerm user = SymbolTerm.intern("user", 1);
private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
private static final SymbolTerm peerDaemon = SymbolTerm.intern("peer_daemon");
@@ -61,8 +59,6 @@ public class PRED_current_user_1 extends Predicate.P1 {
resultTerm = anonymous;
} else if (curUser instanceof PeerDaemonUser) {
resultTerm = peerDaemon;
- } else if (curUser instanceof ReplicationUser) {
- resultTerm = replication;
} else {
throw new EvaluationException("Unknown user type");
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 0a156089c9..3f4b6569b1 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -20,15 +20,12 @@ import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Provider;
+import com.google.inject.util.Providers;
-import com.googlecode.prolog_cafe.lang.HashtableOfTerm;
import com.googlecode.prolog_cafe.lang.IllegalTypeException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.InternalException;
import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
import com.googlecode.prolog_cafe.lang.Operation;
import com.googlecode.prolog_cafe.lang.PInstantiationException;
@@ -39,6 +36,8 @@ import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
+import java.util.Map;
+
/**
* Loads a CurrentUser object for a user identity.
* <p>
@@ -50,10 +49,8 @@ import com.googlecode.prolog_cafe.lang.Term;
* </pre>
*/
class PRED_current_user_2 extends Predicate.P2 {
- private static final long serialVersionUID = 1L;
private static final SymbolTerm user = intern("user", 1);
private static final SymbolTerm anonymous = intern("anonymous");
- private static final SymbolTerm current_user = intern("current_user");
PRED_current_user_2(Term a1, Term a2, Operation n) {
arg1 = a1;
@@ -71,24 +68,14 @@ class PRED_current_user_2 extends Predicate.P2 {
throw new PInstantiationException(this, 1);
}
- HashtableOfTerm userHash = userHash(engine);
- Term userTerm = userHash.get(a1);
- if (userTerm != null && userTerm.isJavaObject()) {
- if (!(((JavaObjectTerm) userTerm).object() instanceof CurrentUser)) {
- userTerm = createUser(engine, a1, userHash);
- }
- } else {
- userTerm = createUser(engine, a1, userHash);
- }
-
- if (!a2.unify(userTerm, engine.trail)) {
+ if (!a2.unify(createUser(engine, a1), engine.trail)) {
return engine.fail();
}
return cont;
}
- public Term createUser(Prolog engine, Term key, HashtableOfTerm userHash) {
+ public Term createUser(Prolog engine, Term key) {
if (!key.isStructure()
|| key.arity() != 1
|| !((StructureTerm) key).functor().equals(user)) {
@@ -98,54 +85,30 @@ class PRED_current_user_2 extends Predicate.P2 {
Term idTerm = key.arg(0);
CurrentUser user;
if (idTerm.isInteger()) {
+ Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
-
- final ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
- IdentifiedUser.GenericFactory userFactory = userFactory(engine);
- if (db != null) {
- user = userFactory.create(new Provider<ReviewDb>() {
- public ReviewDb get() {
- return db;
- }
- }, accountId);
- } else {
- user = userFactory.create(accountId);
+ user = cache.get(accountId);
+ if (user == null) {
+ ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
+ IdentifiedUser.GenericFactory userFactory = userFactory(engine);
+ IdentifiedUser who;
+ if (db != null) {
+ who = userFactory.create(Providers.of(db), accountId);
+ } else {
+ who = userFactory.create(accountId);
+ }
+ cache.put(accountId, who);
+ user = who;
}
-
} else if (idTerm.equals(anonymous)) {
- user = anonymousUser(engine);
+ user = StoredValues.ANONYMOUS_USER.get(engine);
} else {
throw new IllegalTypeException(this, 1, "user(int)", key);
}
- Term userTerm = new JavaObjectTerm(user);
- userHash.put(key, userTerm);
- return userTerm;
- }
-
- private static HashtableOfTerm userHash(Prolog engine) {
- Term userHash = engine.getHashManager().get(current_user);
- if (userHash == null) {
- HashtableOfTerm users = new HashtableOfTerm();
- engine.getHashManager().put(current_user, new JavaObjectTerm(userHash));
- return users;
- }
-
- if (userHash.isJavaObject()) {
- Object obj = ((JavaObjectTerm) userHash).object();
- if (obj instanceof HashtableOfTerm) {
- return (HashtableOfTerm) obj;
- }
- }
-
- throw new InternalException(current_user + " is not HashtableOfTerm");
- }
-
- private static AnonymousUser anonymousUser(Prolog engine) {
- PrologEnvironment env = (PrologEnvironment) engine.control;
- return env.getInjector().getInstance(AnonymousUser.class);
+ return new JavaObjectTerm(user);
}
private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
index 9399865fa9..cbe0fd8bf2 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
@@ -43,8 +43,6 @@ import java.util.List;
* </ul>
*/
class PRED_get_legacy_approval_types_1 extends Predicate.P1 {
- private static final long serialVersionUID = 1L;
-
PRED_get_legacy_approval_types_1(Term a1, Operation n) {
arg1 = a1;
cont = n;
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 3313162045..a75acc0199 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -25,8 +25,7 @@
%% predicate that needs to obtain it.
%%
init :-
- define_hash(commit_labels),
- define_hash(current_user).
+ define_hash(commit_labels).
define_hash(A) :- hash_exists(A), !, hash_clear(A).
define_hash(A) :- atom(A), !, new_hash(_, [alias(A)]).
@@ -98,6 +97,10 @@ index_commit_labels([_ | Rs]) :-
%% Lookup the range allowed to be used.
%%
user_label_range(Label, Who, Min, Max) :-
+ hash_get(commit_labels, '$fast_range', true), !,
+ atom(Label),
+ assume_range_from_label(Label, Who, Min, Max).
+user_label_range(Label, Who, Min, Max) :-
Who = user(_), !,
atom(Label),
current_user(Who, User),
@@ -106,6 +109,14 @@ user_label_range(Label, test_user(Name), Min, Max) :-
clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _)
.
+assume_range_from_label :-
+ hash_put(commit_labels, '$fast_range', true).
+
+assume_range_from_label(Label, Who, Min, Max) :-
+ commit_label(label(Label, Value), Who), !,
+ Min = Value, Max = Value.
+assume_range_from_label(_, _, 0, 0).
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
@@ -138,6 +149,7 @@ call_submit_rule(X, Arg) :- !, F =.. [X, Arg], F.
is_all_ok([]).
is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls).
+is_all_ok([label(_, may(__)) | Ls]) :- is_all_ok(Ls).
is_all_ok(_) :- fail.
@@ -198,8 +210,8 @@ default_submit([Type | Types], Tmp, Out) :-
%%
legacy_submit_rule('MaxWithBlock', Label, Id, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
legacy_submit_rule('MaxNoBlock', Label, Id, Min, Max, T) :- !, max_no_block(Label, Max, T).
-legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = ok(_).
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = may(_).
legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
new file mode 100644
index 0000000000..eada653843
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
@@ -0,0 +1,39 @@
+body {
+ margin: 1em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #527bbd;
+ font-family: sans-serif;
+}
+
+h1, h2, h3 {
+ border-bottom: 2px solid silver;
+}
+
+pre {
+ border: 2px solid silver;
+ background: #ebebeb;
+ margin-left: 2em;
+ width: 100em;
+ color: darkgreen;
+ padding: 2px;
+}
+
+dl dt {
+ margin-top: 1em;
+}
+
+table.plugin_info {
+ border-collapse: separate;
+ border-spacing: 0;
+ text-align: left;
+ margin-left: 2em;
+}
+table.plugin_info th {
+ padding-right: 0.5em;
+ border-right: 2px solid silver;
+}
+table.plugin_info td {
+ padding-left: 0.5em;
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
index f2f0fc76db..1eb6842167 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -31,7 +31,8 @@
## The ChangeFooter.vm template will determine the contents of the footer
## text that will be appended to ALL emails related to changes.
##
---
+#set ($SPACE = " ")
+--$SPACE
#if ($email.changeUrl)
To view, visit $email.changeUrl
#set ($notblank = 1)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
index 547c1b47f3..9af98a6673 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
@@ -43,5 +43,11 @@ Change subject: $change.subject
$email.coverLetter
#end
+##
+## It is possible to increase the span of the quoted lines by using the line
+## count parameter when calling $email.inlineComments as a function.
+##
+## Example: #if($email.inlineComments)$email.getInlineComments(5)#end
+##
#if($email.inlineComments)$email.inlineComments#end
#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
index 8e08dc4e0e..42f2ca9a0c 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -43,6 +43,11 @@ to review the following change.
#end
#else
$fromName has uploaded a new change for review.
+#if($email.changeUrl)
+
+ $email.changeUrl
+
+#end
#end
Change subject: $change.subject
@@ -52,3 +57,7 @@ $email.changeDetail
#if($email.sshHost)
git pull ssh://$email.sshHost/$projectName $patchSet.refName
#end
+#if($email.includeDiff)
+
+$email.UnifiedDiff
+#end \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 212ffb1738..06926df7af 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -17,6 +17,8 @@
# limitations under the License.
#
+unset GREP_OPTIONS
+
CHANGE_ID_AFTER="Bug|Issue"
MSG="$1"
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
new file mode 100644
index 0000000000..24f3386212
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -0,0 +1,54 @@
+// 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;
+
+import junit.framework.TestCase;
+
+public class StringUtilTest extends TestCase {
+ /**
+ * Test the boundary condition that the first character of a string
+ * should be escaped.
+ */
+ public void testEscapeFirstChar() {
+ assertEquals(StringUtil.escapeString("\tLeading tab"), "\\tLeading tab");
+ }
+
+ /**
+ * Test the boundary condition that the last character of a string
+ * should be escaped.
+ */
+ public void testEscapeLastChar() {
+ assertEquals(StringUtil.escapeString("Trailing tab\t"), "Trailing tab\\t");
+ }
+
+ /**
+ * Test that various forms of input strings are escaped (or left as-is)
+ * in the expected way.
+ */
+ public void testEscapeString() {
+ final String[] testPairs =
+ { "", "",
+ "plain string", "plain string",
+ "string with \"quotes\"", "string with \"quotes\"",
+ "string with 'quotes'", "string with 'quotes'",
+ "string with 'quotes'", "string with 'quotes'",
+ "C:\\Program Files\\MyProgram", "C:\\\\Program Files\\\\MyProgram",
+ "string\nwith\nnewlines", "string\\nwith\\nnewlines",
+ "string\twith\ttabs", "string\\twith\\ttabs" };
+ for (int i = 0; i < testPairs.length; i += 2) {
+ assertEquals(StringUtil.escapeString(testPairs[i]), testPairs[i + 1]);
+ }
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index 37197ec9bb..5d7291630d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -26,9 +26,11 @@ import java.util.concurrent.TimeUnit;
public class ConfigUtilTest extends TestCase {
public void testTimeUnit() {
+ assertEquals(ms(0, MILLISECONDS), parse("0"));
assertEquals(ms(2, MILLISECONDS), parse("2ms"));
assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds"));
+ assertEquals(ms(0, SECONDS), parse("0s"));
assertEquals(ms(2, SECONDS), parse("2s"));
assertEquals(ms(231, SECONDS), parse("231sec"));
assertEquals(ms(1, SECONDS), parse("1second"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index ca2b03ac26..a849e689f6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -22,11 +22,13 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupReference;
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.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -43,6 +45,7 @@ import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
+import java.util.Collections;
public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
private final GroupReference developers = new GroupReference(
@@ -70,10 +73,31 @@ public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
+ " exclusiveGroupPermissions = read submit create\n" //
+ " submit = group Developers\n" //
+ " push = group Developers\n" //
- + " read = group Developers\n")) //
+ + " read = group Developers\n" //
+ + "[accounts]\n" //
+ + " sameGroupVisibility = deny group Developers\n" //
+ + " sameGroupVisibility = block group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A simple description\n" //
+ + " accepted = group Developers\n" //
+ + " accepted = group Staff\n" //
+ + " requireContactInformation = true\n" //
+ + " autoVerify = group Developers\n" //
+ + " agreementUrl = http://www.example.com/agree\n")) //
));
ProjectConfig cfg = read(rev);
+ assertEquals(2, cfg.getAccountsSection().getSameGroupVisibility().size());
+ ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+ assertEquals("Individual", ca.getName());
+ assertEquals("A simple description", ca.getDescription());
+ assertEquals("http://www.example.com/agree", ca.getAgreementUrl());
+ assertEquals(2, ca.getAccepted().size());
+ assertEquals(developers, ca.getAccepted().get(0).getGroup());
+ assertEquals("Staff", ca.getAccepted().get(1).getGroup().getName());
+ assertEquals("Developers", ca.getAutoVerify().getName());
+ assertTrue(ca.isRequireContactInformation());
+
AccessSection section = cfg.getAccessSection("refs/heads/*");
assertNotNull("has refs/heads/*", section);
assertNull("no refs/*", cfg.getAccessSection("refs/*"));
@@ -98,14 +122,30 @@ public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
+ " exclusiveGroupPermissions = read submit\n" //
+ " submit = group Developers\n" //
+ " upload = group Developers\n" //
- + " read = group Developers\n")) //
+ + " read = group Developers\n" //
+ + "[accounts]\n" //
+ + " sameGroupVisibility = deny group Developers\n" //
+ + " sameGroupVisibility = block group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A simple description\n" //
+ + " accepted = group Developers\n" //
+ + " requireContactInformation = true\n" //
+ + " autoVerify = group Developers\n" //
+ + " agreementUrl = http://www.example.com/agree\n")) //
));
update(rev);
ProjectConfig cfg = read(rev);
AccessSection section = cfg.getAccessSection("refs/heads/*");
+ cfg.getAccountsSection().setSameGroupVisibility(
+ Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
Permission submit = section.getPermission(Permission.SUBMIT);
submit.add(new PermissionRule(cfg.resolve(staff)));
+ ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+ ca.setRequireContactInformation(false);
+ ca.setAccepted(Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
+ ca.setAutoVerify(null);
+ ca.setDescription("A new description");
rev = commit(cfg);
assertEquals(""//
+ "[access \"refs/heads/*\"]\n" //
@@ -114,6 +154,12 @@ public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
+ "\tsubmit = group Staff\n" //
+ " upload = group Developers\n" //
+ " read = group Developers\n"//
+ + "[accounts]\n" //
+ + " sameGroupVisibility = group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A new description\n" //
+ + " accepted = group Staff\n" //
+ + " agreementUrl = http://www.example.com/agree\n" //
+ "[project]\n"//
+ "\tstate = active\n", text(rev, "project.config"));
}
@@ -156,13 +202,14 @@ public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
private RevCommit commit(ProjectConfig cfg) throws IOException,
MissingObjectException, IncorrectObjectTypeException {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
- cfg.getProject().getNameKey(), //
+ MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ cfg.getProject().getNameKey(),
db);
util.tick(5);
util.setAuthorAndCommitter(md.getCommitBuilder());
md.setMessage("Edit\n");
- assertTrue("commit finished", cfg.commit(md));
+ cfg.commit(md);
Ref ref = db.getRef(GitRepositoryManager.REF_CONFIG);
return util.getRevWalk().parseCommit(ref.getObjectId());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
deleted file mode 100644
index 7ae705fcf0..0000000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2011 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 static com.google.gerrit.server.git.PushReplication.ReplicationConfig.encode;
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.needsUrlEncoding;
-
-import junit.framework.TestCase;
-
-import org.eclipse.jgit.transport.URIish;
-
-import java.net.URISyntaxException;
-
-public class PushReplicationTest extends TestCase {
- public void testNeedsUrlEncoding() throws URISyntaxException {
- assertTrue(needsUrlEncoding(new URIish("http://host/path")));
- assertTrue(needsUrlEncoding(new URIish("https://host/path")));
- assertTrue(needsUrlEncoding(new URIish("amazon-s3://config/bucket/path")));
-
- assertFalse(needsUrlEncoding(new URIish("host:path")));
- assertFalse(needsUrlEncoding(new URIish("user@host:path")));
- assertFalse(needsUrlEncoding(new URIish("git://host/path")));
- assertFalse(needsUrlEncoding(new URIish("ssh://host/path")));
- }
-
- public void testUrlEncoding() {
- assertEquals("foo/bar/thing", encode("foo/bar/thing"));
- assertEquals("--%20All%20Projects%20--", encode("-- All Projects --"));
- assertEquals("name/with%20a%20space", encode("name/with a space"));
- assertEquals("name%0Awith-LF", encode("name\nwith-LF"));
- }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index b32d54c5e6..8d061eb754 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -26,6 +26,7 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.ResultSet;
@@ -71,8 +72,9 @@ public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
private ReviewDb schema;
private Provider<String> urlProvider;
private GitRepositoryManager repoManager;
- private ReplicationQueue replication;
+ private GitReferenceUpdated replication;
+ @SuppressWarnings("unchecked")
@Override
@Before
public void setUp() throws Exception {
@@ -83,7 +85,7 @@ public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
urlProvider = createStrictMock(Provider.class);
repoManager = createStrictMock(GitRepositoryManager.class);
- replication = createStrictMock(ReplicationQueue.class);
+ replication = createStrictMock(GitReferenceUpdated.class);
}
private void doReplay() {
@@ -637,7 +639,7 @@ public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
.andReturn(targetRepository);
- replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+ replication.fire(targetBranchNameKey.getParentKey(),
targetBranchNameKey.get());
expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
@@ -738,7 +740,7 @@ public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
.andReturn(targetRepository);
- replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+ replication.fire(targetBranchNameKey.getParentKey(),
targetBranchNameKey.get());
expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
new file mode 100644
index 0000000000..2d432e635f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -0,0 +1,138 @@
+// 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.ioutil;
+
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class ColumnFormatterTest extends TestCase {
+ /**
+ * Holds an in-memory {@link java.io.PrintWriter} object and allows
+ * comparisons of its contents to a supplied string via an assert statement.
+ */
+ class PrintWriterComparator {
+ private PrintWriter printWriter;
+ private StringWriter stringWriter;
+
+ public PrintWriterComparator() {
+ stringWriter = new StringWriter();
+ printWriter = new PrintWriter(stringWriter);
+ }
+
+ public void assertEquals(String str) {
+ printWriter.flush();
+ TestCase.assertEquals(stringWriter.toString(), str);
+ }
+
+ public PrintWriter getPrintWriter() {
+ return printWriter;
+ }
+ }
+
+ /**
+ * Test that only lines with at least one column of text emit output.
+ */
+ public void testEmptyLine() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.addColumn("foo");
+ formatter.addColumn("bar");
+ formatter.nextLine();
+ formatter.nextLine();
+ formatter.nextLine();
+ formatter.addColumn("foo");
+ formatter.addColumn("bar");
+ formatter.finish();
+ comparator.assertEquals("foo\tbar\nfoo\tbar\n");
+ }
+
+ /**
+ * Test that there is no output if no columns are ever added.
+ */
+ public void testEmptyOutput() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.nextLine();
+ formatter.nextLine();
+ formatter.finish();
+ comparator.assertEquals("");
+ }
+
+ /**
+ * Test that there is no output (nor any exceptions) if we finalize
+ * the output immediately after the creation of the {@link ColumnFormatter}.
+ */
+ public void testNoNextLine() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.finish();
+ comparator.assertEquals("");
+ }
+
+ /**
+ * Test that the text in added columns is escaped while the column separator
+ * (which of course shouldn't be escaped) is left alone.
+ */
+ public void testEscapingTakesPlace() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.addColumn("foo");
+ formatter.addColumn(
+ "\tan indented multi-line\ntext");
+ formatter.nextLine();
+ formatter.finish();
+ comparator.assertEquals("foo\t\\tan indented multi-line\\ntext\n");
+ }
+
+ /**
+ * Test that we get the correct output with multi-line input where the number
+ * of columns in each line varies.
+ */
+ public void testMultiLineDifferentColumnCount() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.addColumn("foo");
+ formatter.addColumn("bar");
+ formatter.addColumn("baz");
+ formatter.nextLine();
+ formatter.addColumn("foo");
+ formatter.addColumn("bar");
+ formatter.nextLine();
+ formatter.finish();
+ comparator.assertEquals("foo\tbar\tbaz\nfoo\tbar\n");
+ }
+
+ /**
+ * Test that we get the correct output with a single column of input.
+ */
+ public void testOneColumn() {
+ final PrintWriterComparator comparator = new PrintWriterComparator();
+ final ColumnFormatter formatter =
+ new ColumnFormatter(comparator.getPrintWriter(), '\t');
+ formatter.addColumn("foo");
+ formatter.nextLine();
+ formatter.finish();
+ comparator.assertEquals("foo\n");
+ }
+}
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 cd6fa699a0..d4b07aefea 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
@@ -20,9 +20,9 @@ import static com.google.gerrit.common.data.Permission.PUSH;
import static com.google.gerrit.common.data.Permission.READ;
import static com.google.gerrit.common.data.Permission.SUBMIT;
-import com.google.common.collect.Iterables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.PermissionRange;
@@ -31,22 +31,18 @@ import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
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.rules.PrologEnvironment;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.cache.ConcurrentHashMapCache;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
@@ -55,11 +51,9 @@ import junit.framework.TestCase;
import org.eclipse.jgit.lib.Config;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -297,6 +291,10 @@ public class RefControlTest extends TestCase {
}
@Override
+ public void remove(Project p) {
+ }
+
+ @Override
public Iterable<Project.NameKey> all() {
return Collections.emptySet();
}
@@ -335,10 +333,9 @@ public class RefControlTest extends TestCase {
local = new ProjectConfig(new Project.NameKey("local"));
local.createInMemory();
- sectionSorter =
- new PermissionCollection.Factory(
- new SectionSortCache(
- new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
+ Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
+ CacheBuilder.newBuilder().build();
+ sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
}
private static void assertOwner(String ref, ProjectControl u) {
@@ -390,12 +387,10 @@ public class RefControlTest extends TestCase {
}
private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
- SchemaFactory<ReviewDb> schema = null;
- GroupCache groupCache = null;
String canonicalWebUrl = "http://localhost";
return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
- Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
+ Collections.<AccountGroup.UUID> emptySet(), projectCache,
sectionSorter,
canonicalWebUrl, new MockUser(name, memberOf),
newProjectState());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index 39cbfe41a7..cc8d47dc07 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -19,6 +19,9 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -27,7 +30,6 @@ import com.google.gerrit.testutil.InMemoryDatabase;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.TypeLiteral;
@@ -63,7 +65,7 @@ public class SchemaUpdaterTest extends TestCase {
final File site = new File(UUID.randomUUID().toString());
final SitePaths paths = new SitePaths(site);
- SchemaUpdater u = Guice.createInjector(new AbstractModule() {
+ SchemaUpdater u = Guice.createInjector(new FactoryModule() {
@Override
protected void configure() {
bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
@@ -88,6 +90,10 @@ public class SchemaUpdaterTest extends TestCase {
bind(GitRepositoryManager.class) //
.to(LocalDiskRepositoryManager.class);
+
+ bind(String.class) //
+ .annotatedWith(AnonymousCowardName.class) //
+ .toProvider(AnonymousCowardNameProvider.class);
}
}).getInstance(SchemaUpdater.class);
@@ -102,6 +108,11 @@ public class SchemaUpdaterTest extends TestCase {
}
@Override
+ public boolean isBatch() {
+ return true;
+ }
+
+ @Override
public void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException {
for (String sql : pruneList) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index c8e684f29b..93d86e5325 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -224,7 +224,7 @@ public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
break;
} else {
expect(repoManager.list()).andReturn(
- new TreeSet<Project.NameKey>(Collections.EMPTY_LIST));
+ new TreeSet<Project.NameKey>(Collections.<Project.NameKey> emptyList()));
}
}
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index a44f84f659..b1f956fc50 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -20,6 +20,8 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -122,6 +124,10 @@ public class InMemoryDatabase implements SchemaFactory<ReviewDb> {
bind(GitRepositoryManager.class) //
.to(LocalDiskRepositoryManager.class);
+
+ bind(String.class) //
+ .annotatedWith(AnonymousCowardName.class) //
+ .toProvider(AnonymousCowardNameProvider.class);
}
}).getBinding(Key.get(SchemaVersion.class, Current.class))
.getProvider().get();
diff --git a/gerrit-sshd/.gitignore b/gerrit-sshd/.gitignore
index 194bedcbc4..8deb9bd230 100644
--- a/gerrit-sshd/.gitignore
+++ b/gerrit-sshd/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-sshd.iml \ No newline at end of file
diff --git a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
index c780f4418c..839d647eef 100644
--- a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 55e8725a3c..a26b1b2b85 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-sshd</artifactId>
@@ -44,6 +44,11 @@ limitations under the License.
</dependency>
<dependency>
+ <groupId>org.apache.mina</groupId>
+ <artifactId>mina-core</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
</dependency>
@@ -67,7 +72,7 @@ limitations under the License.
<dependency>
<groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-ehcache</artifactId>
+ <artifactId>gerrit-cache-h2</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index eb8a5c25c9..af5df2574c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -36,6 +36,9 @@ public abstract class AbstractGitCommand extends BaseCommand {
protected ProjectControl projectControl;
@Inject
+ private SshScope sshScope;
+
+ @Inject
private GitRepositoryManager repoManager;
@Inject
@@ -56,7 +59,7 @@ public abstract class AbstractGitCommand extends BaseCommand {
@Override
public void start(final Environment env) {
Context ctx = context.subContext(newSession(), context.getCommandLine());
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
startThread(new ProjectCommandRunnable() {
@Override
@@ -76,7 +79,7 @@ public abstract class AbstractGitCommand extends BaseCommand {
}
});
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
new file mode 100644
index 0000000000..9582c936a9
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -0,0 +1,125 @@
+// 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.sshd;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Command that executes some other command. */
+public class AliasCommand extends BaseCommand {
+ private final DispatchCommandProvider root;
+ private final Provider<CurrentUser> currentUser;
+ private final CommandName command;
+ private final AtomicReference<Command> atomicCmd;
+
+ AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
+ Provider<CurrentUser> currentUser, CommandName command) {
+ this.root = root;
+ this.currentUser = currentUser;
+ this.command = command;
+ this.atomicCmd = Atomics.newReference();
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ try {
+ begin(env);
+ } catch (UnloggedFailure e) {
+ String msg = e.getMessage();
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
+ err.write(msg.getBytes(ENC));
+ err.flush();
+ onExit(e.exitCode);
+ }
+ }
+
+ private void begin(Environment env) throws UnloggedFailure, IOException {
+ Map<String, Provider<Command>> map = root.getMap();
+ for (String name : chain(command)) {
+ Provider<? extends Command> p = map.get(name);
+ if (p == null) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+
+ Command cmd = p.get();
+ if (!(cmd instanceof DispatchCommand)) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+ map = ((DispatchCommand) cmd).getMap();
+ }
+
+ Provider<? extends Command> p = map.get(command.value());
+ if (p == null) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+
+ Command cmd = p.get();
+ checkRequiresCapability(cmd);
+ if (cmd instanceof BaseCommand) {
+ BaseCommand bc = (BaseCommand)cmd;
+ bc.setName(getName());
+ bc.setArguments(getArguments());
+ }
+ provideStateTo(cmd);
+ atomicCmd.set(cmd);
+ cmd.start(env);
+ }
+
+ @Override
+ public void destroy() {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
+ cmd.destroy();
+ }
+ }
+
+ private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+ RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+ if (rc != null) {
+ CurrentUser user = currentUser.get();
+ CapabilityControl ctl = user.getCapabilities();
+ if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+ String msg = String.format(
+ "fatal: %s does not have \"%s\" capability.",
+ user.getUserName(), rc.value());
+ throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ }
+ }
+ }
+
+ private static LinkedList<String> chain(CommandName command) {
+ LinkedList<String> chain = Lists.newLinkedList();
+ while (command != null) {
+ chain.addFirst(command.value());
+ command = Commands.parentOf(command);
+ }
+ chain.removeLast();
+ return chain;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
index 5839cf17dc..ee28e03aaf 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -15,28 +15,28 @@
package com.google.gerrit.sshd;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.Singleton;
-@Singleton
-class SshCurrentUserProvider implements Provider<CurrentUser> {
- private final Provider<SshSession> session;
- private final Provider<IdentifiedUser> identifiedProvider;
+import org.apache.sshd.server.Command;
+
+/** Resolves an alias to another command. */
+public class AliasCommandProvider implements Provider<Command> {
+ private final CommandName command;
+
+ @Inject
+ @CommandName(Commands.ROOT)
+ private DispatchCommandProvider root;
@Inject
- SshCurrentUserProvider(Provider<SshSession> s, Provider<IdentifiedUser> p) {
- session = s;
- identifiedProvider = p;
+ private Provider<CurrentUser> currentUser;
+
+ public AliasCommandProvider(CommandName command) {
+ this.command = command;
}
@Override
- public CurrentUser get() {
- final CurrentUser user = session.get().getCurrentUser();
- if (user instanceof IdentifiedUser) {
- return identifiedProvider.get();
- }
- return session.get().getCurrentUser();
+ public Command get() {
+ return new AliasCommand(root, currentUser, command);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index a926e776b3..9e04f05edd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
@@ -50,6 +51,7 @@ import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
public abstract class BaseCommand implements Command {
private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
@@ -71,6 +73,9 @@ public abstract class BaseCommand implements Command {
private ExitCallback exit;
@Inject
+ private SshScope sshScope;
+
+ @Inject
private CmdLineParser.Factory cmdLineParserFactory;
@Inject
@@ -87,7 +92,7 @@ public abstract class BaseCommand implements Command {
private Provider<SshScope.Context> contextProvider;
/** The task, as scheduled on a worker thread. */
- private Future<?> task;
+ private final AtomicReference<Future<?>> task;
/** Text of the command line which lead up to invoking this instance. */
private String commandName = "";
@@ -95,6 +100,10 @@ public abstract class BaseCommand implements Command {
/** Unparsed command line options. */
private String[] argv;
+ public BaseCommand() {
+ task = Atomics.newReference();
+ }
+
public void setInputStream(final InputStream in) {
this.in = in;
}
@@ -111,18 +120,27 @@ public abstract class BaseCommand implements Command {
this.exit = callback;
}
+ String getName() {
+ return commandName;
+ }
+
void setName(final String prefix) {
this.commandName = prefix;
}
+ String[] getArguments() {
+ return argv;
+ }
+
public void setArguments(final String[] argv) {
this.argv = argv;
}
@Override
public void destroy() {
- if (task != null && !task.isDone()) {
- task.cancel(true);
+ Future<?> future = task.getAndSet(null);
+ if (future != null && !future.isDone()) {
+ future.cancel(true);
}
}
@@ -243,25 +261,21 @@ public abstract class BaseCommand implements Command {
* @param thunk the runnable to execute on the thread, performing the
* command's logic.
*/
- protected synchronized void startThread(final CommandRunnable thunk) {
+ protected void startThread(final CommandRunnable thunk) {
final TaskThunk tt = new TaskThunk(thunk);
- if (isAdminCommand() || (isAdminHighPriorityCommand()
- && userProvider.get().getCapabilities().canAdministrateServer())) {
+ if (isAdminHighPriorityCommand()
+ && userProvider.get().getCapabilities().canAdministrateServer()) {
// Admin commands should not block the main work threads (there
// might be an interactive shell there), nor should they wait
// for the main work threads.
//
new Thread(tt, tt.toString()).start();
} else {
- task = executor.submit(tt);
+ task.set(executor.submit(tt));
}
}
- private final boolean isAdminCommand() {
- return getClass().getAnnotation(AdminCommand.class) != null;
- }
-
private final boolean isAdminHighPriorityCommand() {
return getClass().getAnnotation(AdminHighPriorityCommand.class) != null;
}
@@ -277,7 +291,9 @@ public abstract class BaseCommand implements Command {
*/
protected void onExit(final int rc) {
exit.onExit(rc);
- cleanup.run();
+ if (cleanup != null) {
+ cleanup.run();
+ }
}
/** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
@@ -355,6 +371,14 @@ public abstract class BaseCommand implements Command {
return new UnloggedFailure(1, "fatal: " + why.getMessage(), why);
}
+ public void checkExclusivity(final Object arg1, final String arg1name,
+ final Object arg2, final String arg2name) throws UnloggedFailure {
+ if (arg1 != null && arg2 != null) {
+ throw new UnloggedFailure(String.format(
+ "%s and %s options are mutually exclusive.", arg1name, arg2name));
+ }
+ }
+
private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
private final CommandRunnable thunk;
private final Context context;
@@ -376,55 +400,59 @@ public abstract class BaseCommand implements Command {
@Override
public void cancel() {
- final Context old = SshScope.set(context);
- try {
- onExit(STATUS_CANCEL);
- } finally {
- SshScope.set(old);
+ synchronized (this) {
+ final Context old = sshScope.set(context);
+ try {
+ onExit(STATUS_CANCEL);
+ } finally {
+ sshScope.set(old);
+ }
}
}
@Override
public void run() {
- final Thread thisThread = Thread.currentThread();
- final String thisName = thisThread.getName();
- int rc = 0;
- final Context old = SshScope.set(context);
- try {
- context.started = System.currentTimeMillis();
- thisThread.setName("SSH " + taskName);
-
- if (thunk instanceof ProjectCommandRunnable) {
- ((ProjectCommandRunnable) thunk).executeParseCommand();
- projectName = ((ProjectCommandRunnable) thunk).getProjectName();
- }
-
+ synchronized (this) {
+ final Thread thisThread = Thread.currentThread();
+ final String thisName = thisThread.getName();
+ int rc = 0;
+ final Context old = sshScope.set(context);
try {
- thunk.run();
- } catch (NoSuchProjectException e) {
- throw new UnloggedFailure(1, e.getMessage() + " no such project");
- } catch (NoSuchChangeException e) {
- throw new UnloggedFailure(1, e.getMessage() + " no such change");
- }
+ context.started = System.currentTimeMillis();
+ thisThread.setName("SSH " + taskName);
+
+ if (thunk instanceof ProjectCommandRunnable) {
+ ((ProjectCommandRunnable) thunk).executeParseCommand();
+ projectName = ((ProjectCommandRunnable) thunk).getProjectName();
+ }
+
+ try {
+ thunk.run();
+ } catch (NoSuchProjectException e) {
+ throw new UnloggedFailure(1, e.getMessage() + " no such project");
+ } catch (NoSuchChangeException e) {
+ throw new UnloggedFailure(1, e.getMessage() + " no such change");
+ }
- out.flush();
- err.flush();
- } catch (Throwable e) {
- try {
out.flush();
- } catch (Throwable e2) {
- }
- try {
err.flush();
- } catch (Throwable e2) {
- }
- rc = handleError(e);
- } finally {
- try {
- onExit(rc);
+ } catch (Throwable e) {
+ try {
+ out.flush();
+ } catch (Throwable e2) {
+ }
+ try {
+ err.flush();
+ } catch (Throwable e2) {
+ }
+ rc = handleError(e);
} finally {
- SshScope.set(old);
- thisThread.setName(thisName);
+ try {
+ onExit(rc);
+ } finally {
+ sshScope.set(old);
+ thisThread.setName(thisName);
+ }
}
}
}
@@ -505,6 +533,15 @@ public abstract class BaseCommand implements Command {
/**
* Create a new failure.
*
+ * @param msg message to also send to the client's stderr.
+ */
+ public UnloggedFailure(final String msg) {
+ this(1, msg);
+ }
+
+ /**
+ * Create a new failure.
+ *
* @param exitCode exit code to return the client, which indicates the
* failure status of this command. Should be between 1 and 255,
* inclusive.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 66e6addf03..7f08d49bdc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -53,6 +53,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
private final DispatchCommandProvider dispatcher;
private final SshLog log;
+ private final SshScope sshScope;
private final ScheduledExecutorService startExecutor;
private final Executor destroyExecutor;
@@ -60,9 +61,10 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
CommandFactoryProvider(
@CommandName(Commands.ROOT) final DispatchCommandProvider d,
@GerritServerConfig final Config cfg, final WorkQueue workQueue,
- final SshLog l) {
+ final SshLog l, final SshScope s) {
dispatcher = d;
log = l;
+ sshScope = s;
int threads = cfg.getInt("sshd","commandStartThreads", 2);
startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -120,7 +122,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
public void setSession(final ServerSession session) {
final SshSession s = session.getAttribute(SshSession.KEY);
- this.ctx = new Context(s, commandLine);
+ this.ctx = sshScope.newContext(s, commandLine);
}
public void start(final Environment env) throws IOException {
@@ -145,7 +147,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
private void onStart() throws IOException {
synchronized (this) {
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
cmd = dispatcher.get();
cmd.setArguments(argv);
@@ -167,7 +169,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
});
cmd.start(env);
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
}
@@ -211,14 +213,14 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
private void onDestroy() {
synchronized (this) {
if (cmd != null) {
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
cmd.destroy();
log(BaseCommand.STATUS_CANCEL);
} finally {
ctx = null;
cmd = null;
- SshScope.set(old);
+ sshScope.set(old);
}
}
}
@@ -226,7 +228,7 @@ class CommandFactoryProvider implements Provider<CommandFactory> {
}
/** Split a command line into a string array. */
- static String[] split(String commandLine) {
+ static public String[] split(String commandLine) {
final List<String> list = new ArrayList<String>();
boolean inquote = false;
boolean inDblQuote = false;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 12cff9cbd2..5340d6f340 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -97,6 +97,13 @@ public class Commands {
return false;
}
+ static CommandName parentOf(CommandName name) {
+ if (name instanceof NestedCommandNameImpl) {
+ return ((NestedCommandNameImpl) name).parent;
+ }
+ return null;
+ }
+
private static final class NestedCommandNameImpl implements CommandName {
private final CommandName parent;
private final String name;
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 a96a661766..a69a2f1e38 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
@@ -65,6 +65,7 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
private final IdentifiedUser.GenericFactory userFactory;
private final PeerDaemonUser.Factory peerFactory;
private final Config config;
+ private final SshScope sshScope;
private final Set<PublicKey> myHostKeys;
private volatile PeerKeyCache peerKeyCache;
@@ -72,12 +73,13 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
final SitePaths site, final KeyPairProvider hostKeyProvider,
- final @GerritServerConfig Config cfg) {
+ final @GerritServerConfig Config cfg, final SshScope s) {
sshKeyCache = skc;
sshLog = l;
userFactory = uf;
peerFactory = pf;
config = cfg;
+ sshScope = s;
myHostKeys = myHostKeys(hostKeyProvider);
peerKeyCache = new PeerKeyCache(site.peer_keys);
}
@@ -171,24 +173,24 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator {
// session, record a login event in the log and add
// a close listener to record a logout event.
//
- Context ctx = new Context(sd, null);
- Context old = SshScope.set(ctx);
+ Context ctx = sshScope.newContext(sd, null);
+ Context old = sshScope.set(ctx);
try {
sshLog.onLogin();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
session.getIoSession().getCloseFuture().addListener(
new IoFutureListener<IoFuture>() {
@Override
public void operationComplete(IoFuture future) {
- final Context ctx = new Context(sd, null);
- final Context old = SshScope.set(ctx);
+ final Context ctx = sshScope.newContext(sd, null);
+ final Context old = sshScope.set(ctx);
try {
sshLog.onLogout();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
});
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 8daa7f4580..691f3a00df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,8 +14,12 @@
package com.google.gerrit.sshd;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.sshd.args4j.SubcommandHandler;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.args4j.SubcommandHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
@@ -28,19 +32,19 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Command that dispatches to a subcommand from its command table.
*/
final class DispatchCommand extends BaseCommand {
interface Factory {
- DispatchCommand create(String prefix, Map<String, Provider<Command>> map);
+ DispatchCommand create(Map<String, Provider<Command>> map);
}
private final Provider<CurrentUser> currentUser;
- private final String prefix;
private final Map<String, Provider<Command>> commands;
- private Command cmd;
+ private final AtomicReference<Command> atomicCmd;
@Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
private String commandName;
@@ -49,11 +53,15 @@ final class DispatchCommand extends BaseCommand {
private List<String> args = new ArrayList<String>();
@Inject
- DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
+ DispatchCommand(final Provider<CurrentUser> cu,
@Assisted final Map<String, Provider<Command>> all) {
currentUser = cu;
- prefix = pfx;
commands = all;
+ atomicCmd = Atomics.newReference();
+ }
+
+ Map<String, Provider<Command>> getMap() {
+ return commands;
}
@Override
@@ -64,25 +72,19 @@ final class DispatchCommand extends BaseCommand {
final Provider<Command> p = commands.get(commandName);
if (p == null) {
String msg =
- (prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+ (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
+ commandName + ": not found";
throw new UnloggedFailure(1, msg);
}
final Command cmd = p.get();
-
- if (isAdminCommand(cmd)
- && !currentUser.get().getCapabilities().canAdministrateServer()) {
- final String msg = "fatal: Not a Gerrit administrator";
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
+ checkRequiresCapability(cmd);
if (cmd instanceof BaseCommand) {
final BaseCommand bc = (BaseCommand) cmd;
- if (prefix.isEmpty())
+ if (getName().isEmpty())
bc.setName(commandName);
else
- bc.setName(prefix + " " + commandName);
+ bc.setName(getName() + " " + commandName);
bc.setArguments(args.toArray(new String[args.size()]));
} else if (!args.isEmpty()) {
@@ -90,10 +92,7 @@ final class DispatchCommand extends BaseCommand {
}
provideStateTo(cmd);
-
- synchronized (this) {
- this.cmd = cmd;
- }
+ atomicCmd.set(cmd);
cmd.start(env);
} catch (UnloggedFailure e) {
@@ -107,17 +106,25 @@ final class DispatchCommand extends BaseCommand {
}
}
- private boolean isAdminCommand(final Command cmd) {
- return cmd.getClass().getAnnotation(AdminCommand.class) != null;
+ private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+ RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+ if (rc != null) {
+ CurrentUser user = currentUser.get();
+ CapabilityControl ctl = user.getCapabilities();
+ if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+ String msg = String.format(
+ "fatal: %s does not have \"%s\" capability.",
+ user.getUserName(), rc.value());
+ throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ }
+ }
}
@Override
public void destroy() {
- synchronized (this) {
- if (cmd != null) {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
cmd.destroy();
- cmd = null;
- }
}
}
@@ -125,26 +132,30 @@ final class DispatchCommand extends BaseCommand {
protected String usage() {
final StringBuilder usage = new StringBuilder();
usage.append("Available commands");
- if (!prefix.isEmpty()) {
+ if (!getName().isEmpty()) {
usage.append(" of ");
- usage.append(prefix);
+ usage.append(getName());
}
usage.append(" are:\n");
usage.append("\n");
- for (Map.Entry<String, Provider<Command>> e : commands.entrySet()) {
+ for (String name : Sets.newTreeSet(commands.keySet())) {
usage.append(" ");
- usage.append(e.getKey());
+ usage.append(name);
usage.append("\n");
}
usage.append("\n");
usage.append("See '");
- if (prefix.indexOf(' ') < 0) {
- usage.append(prefix);
+ if (getName().indexOf(' ') < 0) {
+ usage.append(getName());
usage.append(' ');
}
usage.append("COMMAND --help' for more information.\n");
usage.append("\n");
return usage.toString();
}
+
+ public String getCommandName() {
+ return commandName;
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 0b69228b85..b76ff71e78 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.inject.Binding;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -23,11 +25,8 @@ import com.google.inject.TypeLiteral;
import org.apache.sshd.server.Command;
import java.lang.annotation.Annotation;
-import java.util.Collections;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.concurrent.ConcurrentMap;
/**
* Creates DispatchCommand using commands registered by {@link CommandModule}.
@@ -39,27 +38,45 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
@Inject
private DispatchCommand.Factory factory;
- private final String dispatcherName;
private final CommandName parent;
-
- private volatile Map<String, Provider<Command>> map;
+ private volatile ConcurrentMap<String, Provider<Command>> map;
public DispatchCommandProvider(final CommandName cn) {
- this(Commands.nameOf(cn), cn);
- }
-
- public DispatchCommandProvider(final String dispatcherName,
- final CommandName cn) {
- this.dispatcherName = dispatcherName;
this.parent = cn;
}
@Override
public DispatchCommand get() {
- return factory.create(dispatcherName, getMap());
+ return factory.create(getMap());
+ }
+
+ public RegistrationHandle register(final CommandName name,
+ final Provider<Command> cmd) {
+ final ConcurrentMap<String, Provider<Command>> m = getMap();
+ if (m.putIfAbsent(name.value(), cmd) != null) {
+ throw new IllegalArgumentException(name.value() + " exists");
+ }
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ m.remove(name.value(), cmd);
+ }
+ };
+ }
+
+ public RegistrationHandle replace(final CommandName name,
+ final Provider<Command> cmd) {
+ final ConcurrentMap<String, Provider<Command>> m = getMap();
+ m.put(name.value(), cmd);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {
+ m.remove(name.value(), cmd);
+ }
+ };
}
- private Map<String, Provider<Command>> getMap() {
+ ConcurrentMap<String, Provider<Command>> getMap() {
if (map == null) {
synchronized (this) {
if (map == null) {
@@ -71,10 +88,8 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
}
@SuppressWarnings("unchecked")
- private Map<String, Provider<Command>> createMap() {
- final Map<String, Provider<Command>> m =
- new TreeMap<String, Provider<Command>>();
-
+ private ConcurrentMap<String, Provider<Command>> createMap() {
+ ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
for (final Binding<?> b : allCommands()) {
final Annotation annotation = b.getKey().getAnnotation();
if (annotation instanceof CommandName) {
@@ -84,9 +99,7 @@ public class DispatchCommandProvider implements Provider<DispatchCommand> {
}
}
}
-
- return Collections.unmodifiableMap(
- new LinkedHashMap<String, Provider<Command>>(m));
+ return m;
}
private static final TypeLiteral<Command> type =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index be513b33a2..d2fdf781d1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -58,6 +58,7 @@ class NoShell implements Factory<Command> {
static class SendMessage implements Command, SessionAware {
private final Provider<MessageFactory> messageFactory;
+ private final SshScope sshScope;
private InputStream in;
private OutputStream out;
@@ -66,8 +67,9 @@ class NoShell implements Factory<Command> {
private Context context;
@Inject
- SendMessage(Provider<MessageFactory> messageFactory) {
+ SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
this.messageFactory = messageFactory;
+ this.sshScope = sshScope;
}
public void setInputStream(final InputStream in) {
@@ -87,16 +89,16 @@ class NoShell implements Factory<Command> {
}
public void setSession(final ServerSession session) {
- this.context = new Context(session.getAttribute(SshSession.KEY), "");
+ this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
}
public void start(final Environment env) throws IOException {
- Context old = SshScope.set(context);
+ Context old = sshScope.set(context);
String message;
try {
message = messageFactory.get().getMessage();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
err.write(Constants.encode(message.toString()));
err.flush();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
new file mode 100644
index 0000000000..4dbb8d76d8
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -0,0 +1,49 @@
+// 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.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.sshd.CommandName;
+import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.binder.LinkedBindingBuilder;
+
+import org.apache.sshd.server.Command;
+
+import javax.inject.Inject;
+
+public abstract class PluginCommandModule extends AbstractModule {
+ private CommandName command;
+
+ @Inject
+ void setPluginName(@PluginName String name) {
+ this.command = Commands.named(name);
+ }
+
+ @Override
+ protected final void configure() {
+ Preconditions.checkState(command != null, "@PluginName must be provided");
+ bind(Commands.key(command)).toProvider(new DispatchCommandProvider(command));
+ configureCommands();
+ }
+
+ protected abstract void configureCommands();
+
+ protected LinkedBindingBuilder<Command> command(String subCmd) {
+ return bind(Commands.key(command, subCmd));
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000000..03485f71cb
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -0,0 +1,74 @@
+// 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.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+
+import org.apache.sshd.server.Command;
+
+import java.util.Map;
+
+class SshAutoRegisterModuleGenerator
+ extends AbstractModule
+ implements ModuleGenerator {
+ private final Map<String, Class<Command>> commands = Maps.newHashMap();
+ private CommandName command;
+
+ @Override
+ protected void configure() {
+ bind(Commands.key(command))
+ .toProvider(new DispatchCommandProvider(command));
+ for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
+ bind(Commands.key(command, e.getKey())).to(e.getValue());
+ }
+ }
+
+ public void setPluginName(String name) {
+ command = Commands.named(name);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void export(Export export, Class<?> type)
+ throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ if (Command.class.isAssignableFrom(type)) {
+ Class<Command> old = commands.get(export.value());
+ if (old != null) {
+ throw new InvalidPluginException(String.format(
+ "@Export(\"%s\") has duplicate bindings:\n %s\n %s",
+ export.value(), old.getName(), type.getName()));
+ }
+ commands.put(export.value(), (Class<Command>) type);
+ } else {
+ throw new InvalidPluginException(String.format(
+ "Class %s with @Export(\"%s\") must extend %s or implement %s",
+ type.getName(), export.value(),
+ SshCommand.class.getName(), Command.class.getName()));
+ }
+ }
+
+ @Override
+ public Module create() throws InvalidPluginException {
+ Preconditions.checkState(command != null, "pluginName must be provided");
+ return !commands.isEmpty() ? this : null;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java
new file mode 100644
index 0000000000..f6209babff
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java
@@ -0,0 +1,45 @@
+// 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.sshd;
+
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public abstract class SshCommand extends BaseCommand {
+ protected PrintWriter stdout;
+ protected PrintWriter stderr;
+
+ @Override
+ public void start(Environment env) throws IOException {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ stdout = toPrintWriter(out);
+ stderr = toPrintWriter(err);
+ try {
+ SshCommand.this.run();
+ } finally {
+ stdout.flush();
+ stderr.flush();
+ }
+ }
+ });
+ }
+
+ protected abstract void run() throws UnloggedFailure, Failure, Exception;
+}
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 d382a575fd..664ce456bd 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
@@ -18,7 +18,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
deleted file mode 100644
index 516b59cb43..0000000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
+++ /dev/null
@@ -1,47 +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.sshd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshIdentifiedUserProvider implements Provider<IdentifiedUser> {
- private final Provider<SshSession> session;
- private final IdentifiedUser.RequestFactory factory;
-
- @Inject
- SshIdentifiedUserProvider(Provider<SshSession> s,
- IdentifiedUser.RequestFactory f) {
- session = s;
- factory = f;
- }
-
- @Override
- public IdentifiedUser get() {
- final CurrentUser user = session.get().getCurrentUser();
- if (user instanceof IdentifiedUser) {
- return factory.create(user.getAccessPath(), //
- ((IdentifiedUser) user).getAccountId());
- }
- throw new ProvisionException(NotSignedInException.MESSAGE,
- new NotSignedInException());
- }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 1f5ac28e50..0a1f708b44 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -16,13 +16,13 @@ package com.google.gerrit.sshd;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -42,6 +42,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ExecutionException;
/** Provides the {@link SshKeyCacheEntry}. */
@Singleton
@@ -57,9 +58,10 @@ public class SshKeyCacheImpl implements SshKeyCache {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
- new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
- core(type, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME,
+ String.class,
+ new TypeLiteral<Iterable<SshKeyCacheEntry>>(){})
+ .loader(Loader.class);
bind(SshKeyCacheImpl.class);
bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
}
@@ -71,20 +73,27 @@ public class SshKeyCacheImpl implements SshKeyCache {
.asList(new SshKeyCacheEntry[0]));
}
- private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
+ private final LoadingCache<String, Iterable<SshKeyCacheEntry>> cache;
@Inject
SshKeyCacheImpl(
- @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+ @Named(CACHE_NAME) LoadingCache<String, Iterable<SshKeyCacheEntry>> cache) {
this.cache = cache;
}
- public Iterable<SshKeyCacheEntry> get(String username) {
- return cache.get(username);
+ Iterable<SshKeyCacheEntry> get(String username) {
+ try {
+ return cache.get(username);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load SSH keys for " + username, e);
+ return Collections.emptyList();
+ }
}
public void evict(String username) {
- cache.remove(username);
+ if (username != null) {
+ cache.invalidate(username);
+ }
}
@Override
@@ -107,7 +116,7 @@ public class SshKeyCacheImpl implements SshKeyCache {
}
}
- static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+ static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -116,8 +125,7 @@ public class SshKeyCacheImpl implements SshKeyCache {
}
@Override
- public Iterable<SshKeyCacheEntry> createEntry(String username)
- throws Exception {
+ public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountExternalId.Key key =
@@ -143,11 +151,6 @@ public class SshKeyCacheImpl implements SshKeyCache {
}
}
- @Override
- public Iterable<SshKeyCacheEntry> missing(String username) {
- return Collections.emptyList();
- }
-
private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
try {
kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 923ac987ca..c9ac3e77a3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -14,7 +14,9 @@
package com.google.gerrit.sshd;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
@@ -40,6 +42,7 @@ import org.eclipse.jgit.util.QuotedString;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
+import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
@@ -58,12 +61,14 @@ class SshLog implements LifecycleListener {
private final Provider<SshSession> session;
private final Provider<Context> context;
private final AsyncAppender async;
+ private final AuditService auditService;
@Inject
SshLog(final Provider<SshSession> session, final Provider<Context> context,
- final SitePaths site, @GerritServerConfig Config config) {
+ final SitePaths site, @GerritServerConfig Config config, AuditService auditService) {
this.session = session;
this.context = context;
+ this.auditService = auditService;
final DailyRollingFileAppender dst = new DailyRollingFileAppender();
dst.setName(LOG_NAME);
@@ -96,6 +101,7 @@ class SshLog implements LifecycleListener {
void onLogin() {
async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
+ audit(context.get(), "0", "LOGIN", new String[] {});
}
void onAuthFail(final SshSession sd) {
@@ -121,6 +127,7 @@ class SshLog implements LifecycleListener {
}
async.append(event);
+ audit(null, "FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
}
void onExecute(int exitValue) {
@@ -158,10 +165,19 @@ class SshLog implements LifecycleListener {
event.setProperty(P_STATUS, status);
async.append(event);
+ audit(context.get(), status, getCommand(commandLine),
+ CommandFactoryProvider.split(commandLine));
+ }
+
+ private String getCommand(String commandLine) {
+ commandLine = commandLine.trim();
+ int spacePos = commandLine.indexOf(' ');
+ return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
}
void onLogout() {
async.append(log("LOGOUT"));
+ audit(context.get(), "0", "LOGOUT", new String[] {});
}
private LoggingEvent log(final String msg) {
@@ -192,7 +208,6 @@ class SshLog implements LifecycleListener {
} else if (user instanceof PeerDaemonUser) {
userName = PeerDaemonUser.USER_NAME;
-
}
event.setProperty(P_USER_NAME, userName);
@@ -400,4 +415,43 @@ class SshLog implements LifecycleListener {
public void setLogger(Logger logger) {
}
}
+
+ void audit(Context ctx, Object result, String commandName, String[] args) {
+ final String sid = extractSessionId(ctx);
+ final long created = extractCreated(ctx);
+ final String what = extractWhat(commandName, args);
+ auditService.dispatch(new AuditEvent(sid, extractCurrentUser(ctx), "ssh:"
+ + what, created, Arrays.asList(args), result));
+ }
+
+ private String extractWhat(String commandName, String[] args) {
+ String result = commandName;
+ if ("gerrit".equals(commandName)) {
+ if (args.length > 1)
+ result = "gerrit"+"."+args[1];
+ }
+ return result;
+ }
+
+ private long extractCreated(final Context ctx) {
+ return (ctx != null) ? ctx.created : System.currentTimeMillis();
+ }
+
+ private CurrentUser extractCurrentUser(final Context ctx) {
+ if (ctx != null) {
+ SshSession session = ctx.getSession();
+ return (session == null) ? null : session.getCurrentUser();
+ } else {
+ return null;
+ }
+ }
+
+ private String extractSessionId(final Context ctx) {
+ if (ctx != null) {
+ SshSession session = ctx.getSession();
+ return (session == null) ? null : IdGenerator.format(session.getSessionId());
+ } else {
+ return null;
+ }
+ }
}
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 54b0bb5ab4..7f4a1f7a31 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
@@ -15,52 +15,59 @@
package com.google.gerrit.sshd;
import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
+import com.google.common.collect.Maps;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CmdLineParserModule;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.QueueProvider;
import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.RequestScopePropagator;
-import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
-import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
-import com.google.gerrit.sshd.args4j.AccountIdHandler;
-import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
-import com.google.gerrit.sshd.args4j.ProjectControlHandler;
-import com.google.gerrit.sshd.args4j.SocketAddressHandler;
import com.google.gerrit.sshd.commands.DefaultCommandModule;
import com.google.gerrit.sshd.commands.QueryShell;
-import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gerrit.util.cli.OptionHandlerUtil;
+import com.google.inject.Inject;
+import com.google.inject.internal.UniqueAnnotations;
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.kohsuke.args4j.spi.OptionHandler;
-
+import org.eclipse.jgit.lib.Config;
import java.net.SocketAddress;
+import java.util.Map;
/** Configures standard dependencies for {@link SshDaemon}. */
public class SshModule extends FactoryModule {
+ private final Map<String, String> aliases;
+
+ @Inject
+ SshModule(@GerritServerConfig Config cfg) {
+ aliases = Maps.newHashMap();
+ for (String name : cfg.getNames("ssh-alias")) {
+ aliases.put(name, cfg.getString("ssh-alias", null, name));
+ }
+ }
+
@Override
protected void configure() {
bindScope(RequestScoped.class, SshScope.REQUEST);
bind(RequestScopePropagator.class).to(SshScope.Propagator.class);
+ bind(SshScope.class).in(SINGLETON);
configureRequestScope();
- configureCmdLineParser();
+ install(new CmdLineParserModule());
+ configureAliases();
install(SshKeyCacheImpl.module());
bind(SshLog.class);
@@ -70,7 +77,7 @@ public class SshModule extends FactoryModule {
factory(PeerDaemonUser.Factory.class);
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
- .toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
+ .toInstance(new DispatchCommandProvider(Commands.CMD_ROOT));
bind(CommandFactoryProvider.class);
bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
@@ -87,12 +94,37 @@ public class SshModule extends FactoryModule {
install(new LifecycleModule() {
@Override
protected void configure() {
+ bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
+ bind(SshPluginStarterCallback.class);
+ bind(StartPluginListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(SshPluginStarterCallback.class);
+
+ bind(ReloadPluginListener.class)
+ .annotatedWith(UniqueAnnotations.create())
+ .to(SshPluginStarterCallback.class);
+
+ listener().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
}
});
}
+ private void configureAliases() {
+ CommandName gerrit = Commands.named("gerrit");
+ for (Map.Entry<String, String> e : aliases.entrySet()) {
+ String name = e.getKey();
+ String[] dest = e.getValue().split("[ \\t]+");
+ CommandName cmd = Commands.named(dest[0]);
+ for (int i = 1; i < dest.length; i++) {
+ cmd = Commands.named(cmd, dest[i]);
+ }
+ bind(Commands.key(gerrit, name))
+ .toProvider(new AliasCommandProvider(cmd));
+ }
+ }
+
private void configureRequestScope() {
bind(SshScope.Context.class).toProvider(SshScope.ContextProvider.class);
@@ -101,30 +133,9 @@ public class SshModule extends FactoryModule {
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
SshRemotePeerProvider.class).in(SshScope.REQUEST);
- bind(CurrentUser.class).toProvider(SshCurrentUserProvider.class).in(
- SshScope.REQUEST);
- bind(IdentifiedUser.class).toProvider(SshIdentifiedUserProvider.class).in(
- SshScope.REQUEST);
-
bind(WorkQueue.Executor.class).annotatedWith(CommandExecutor.class)
.toProvider(CommandExecutorProvider.class).in(SshScope.REQUEST);
install(new GerritRequestModule());
}
-
- private void configureCmdLineParser() {
- factory(CmdLineParser.Factory.class);
-
- registerOptionHandler(Account.Id.class, AccountIdHandler.class);
- registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
- registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
- registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
- registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
- registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
- }
-
- private <T> void registerOptionHandler(Class<T> type,
- Class<? extends OptionHandler<T>> impl) {
- install(OptionHandlerUtil.moduleFor(type, impl));
- }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
new file mode 100644
index 0000000000..4f9fe3319d
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -0,0 +1,73 @@
+// 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.sshd;
+
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+
+@Singleton
+class SshPluginStarterCallback
+ implements StartPluginListener, ReloadPluginListener {
+ private static final Logger log = LoggerFactory
+ .getLogger(SshPluginStarterCallback.class);
+
+ private final DispatchCommandProvider root;
+
+ @Inject
+ SshPluginStarterCallback(
+ @CommandName(Commands.ROOT) DispatchCommandProvider root) {
+ this.root = root;
+ }
+
+ @Override
+ public void onStartPlugin(Plugin plugin) {
+ Provider<Command> cmd = load(plugin);
+ if (cmd != null) {
+ plugin.add(root.register(Commands.named(plugin.getName()), cmd));
+ }
+ }
+
+ @Override
+ public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+ Provider<Command> cmd = load(newPlugin);
+ if (cmd != null) {
+ newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
+ }
+ }
+
+ private Provider<Command> load(Plugin plugin) {
+ if (plugin.getSshInjector() != null) {
+ Key<Command> key = Commands.key(plugin.getName());
+ try {
+ return plugin.getSshInjector().getProvider(key);
+ } catch (RuntimeException err) {
+ log.warn(String.format(
+ "Plugin %s did not define its top-level command",
+ plugin.getName()), err);
+ }
+ }
+ return null;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index 92609b5235..d6f66ca1b6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -14,8 +14,13 @@
package com.google.gerrit.sshd;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
@@ -26,10 +31,10 @@ import java.util.Map;
/** Guice scopes for state during an SSH connection. */
class SshScope {
- static class Context {
- private static final Key<RequestCleanup> RC_KEY =
- Key.get(RequestCleanup.class);
+ private static final Key<RequestCleanup> RC_KEY =
+ Key.get(RequestCleanup.class);
+ class Context implements RequestContext {
private final RequestCleanup cleanup;
private final SshSession session;
private final String commandLine;
@@ -56,10 +61,6 @@ class SshScope {
finished = p.finished;
}
- Context(final SshSession s, final String c) {
- this(s, c, System.currentTimeMillis());
- }
-
String getCommandLine() {
return commandLine;
}
@@ -68,6 +69,16 @@ class SshScope {
return session;
}
+ @Override
+ public CurrentUser getCurrentUser() {
+ final CurrentUser user = session.getCurrentUser();
+ if (user instanceof IdentifiedUser) {
+ return userFactory.create(user.getAccessPath(), //
+ ((IdentifiedUser) user).getAccountId());
+ }
+ return user;
+ }
+
synchronized <T> T get(Key<T> key, Provider<T> creator) {
@SuppressWarnings("unchecked")
T t = (T) map.get(key);
@@ -100,15 +111,19 @@ class SshScope {
}
static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
- Propagator() {
- super(REQUEST, current);
+ private final SshScope sshScope;
+
+ @Inject
+ Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
+ super(REQUEST, current, local);
+ this.sshScope = sshScope;
}
@Override
protected Context continuingContext(Context ctx) {
// The cleanup is not chained, since the RequestScopePropagator executors
// the Context's cleanup when finished executing.
- return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+ return sshScope.newContinuingContext(ctx);
}
}
@@ -123,9 +138,28 @@ class SshScope {
return ctx;
}
- static Context set(Context ctx) {
+ private final ThreadLocalRequestContext local;
+ private final IdentifiedUser.RequestFactory userFactory;
+
+ @Inject
+ SshScope(ThreadLocalRequestContext local,
+ IdentifiedUser.RequestFactory userFactory) {
+ this.local = local;
+ this.userFactory = userFactory;
+ }
+
+ Context newContext(SshSession session, String commandLine) {
+ return new Context(session, commandLine, System.currentTimeMillis());
+ }
+
+ private Context newContinuingContext(Context ctx) {
+ return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+ }
+
+ Context set(Context ctx) {
Context old = current.get();
current.set(ctx);
+ local.setContext(ctx);
return old;
}
@@ -149,7 +183,4 @@ class SshScope {
return "SshScopes.REQUEST";
}
};
-
- private SshScope() {
- }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 492966ea4d..33459c2529 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
@@ -32,6 +33,7 @@ import java.io.IOException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Executes any other command as a different user identity.
@@ -41,6 +43,7 @@ import java.util.List;
* key, or a key on this daemon's peer host key ring.
*/
public final class SuExec extends BaseCommand {
+ private final SshScope sshScope;
private final DispatchCommandProvider dispatcher;
private Provider<CurrentUser> caller;
@@ -57,18 +60,21 @@ public final class SuExec extends BaseCommand {
@Argument(index = 0, multiValued = true, metaVar = "COMMAND")
private List<String> args = new ArrayList<String>();
- private Command cmd;
+ private final AtomicReference<Command> atomicCmd;
@Inject
- SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
+ SuExec(final SshScope sshScope,
+ @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
final Provider<CurrentUser> caller, final Provider<SshSession> session,
final IdentifiedUser.GenericFactory userFactory,
final SshScope.Context callingContext) {
+ this.sshScope = sshScope;
this.dispatcher = dispatcher;
this.caller = caller;
this.session = session;
this.userFactory = userFactory;
this.callingContext = callingContext;
+ atomicCmd = Atomics.newReference();
}
@Override
@@ -78,18 +84,15 @@ public final class SuExec extends BaseCommand {
parseCommandLine();
final Context ctx = callingContext.subContext(newSession(), join(args));
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
final BaseCommand cmd = dispatcher.get();
cmd.setArguments(args.toArray(new String[args.size()]));
provideStateTo(cmd);
-
- synchronized (this) {
- this.cmd = cmd;
- }
+ atomicCmd.set(cmd);
cmd.start(env);
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
} else {
@@ -136,11 +139,9 @@ public final class SuExec extends BaseCommand {
@Override
public void destroy() {
- synchronized (this) {
- if (cmd != null) {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
cmd.destroy();
- cmd = null;
- }
}
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 545554caf3..08c650cde9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -14,16 +14,18 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.sshd.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
/** Opens a query processor. */
-@AdminCommand
-final class AdminQueryShell extends BaseCommand {
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminQueryShell extends SshCommand {
@Inject
private QueryShell.Factory factory;
@@ -34,19 +36,13 @@ final class AdminQueryShell extends BaseCommand {
private String query;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- final QueryShell shell = factory.create(in, out);
- shell.setOutputFormat(format);
- if (query != null) {
- shell.execute(query);
- } else {
- shell.run();
- }
- }
- });
+ protected void run() {
+ final QueryShell shell = factory.create(in, out);
+ shell.setOutputFormat(format);
+ if (query != null) {
+ shell.execute(query);
+ } else {
+ shell.run();
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 950bf341f1..6483e247c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
@@ -21,11 +23,9 @@ import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.sshd.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
@@ -34,14 +34,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-@AdminCommand
-final class AdminSetParent extends BaseCommand {
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminSetParent extends SshCommand {
private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
@Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
@@ -68,26 +67,10 @@ final class AdminSetParent extends BaseCommand {
@Inject
private AllProjectsName allProjectsName;
- private PrintWriter stdout;
private Project.NameKey newParentKey = null;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- stdout = toPrintWriter(out);
- try {
- parseCommandLine();
- updateParents();
- } finally {
- stdout.flush();
- }
- }
- });
- }
-
- private void updateParents() throws Failure {
+ protected void run() throws Failure {
if (oldParent == null && children.isEmpty()) {
throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
"arguments or the --children-of option has to be set");
@@ -154,9 +137,7 @@ final class AdminSetParent extends BaseCommand {
config.getProject().setParentName(newParentKey);
md.setMessage("Inherit access from "
+ (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
- if (!config.commit(md)) {
- err.append("error: Could not update project " + name + "\n");
- }
+ config.commit(md);
} finally {
md.close();
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
new file mode 100644
index 0000000000..939d68ae09
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -0,0 +1,101 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BanCommitCommand extends SshCommand {
+ @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
+ private String reason;
+
+ @Argument(index = 0, required = true, metaVar = "PROJECT",
+ usage = "name of the project for which the commit should be banned")
+ private ProjectControl projectControl;
+
+ @Argument(index = 1, required = true, multiValued = true, metaVar = "COMMIT",
+ usage = "commit(s) that should be banned")
+ private List<ObjectId> commitsToBan = new ArrayList<ObjectId>();
+
+ @Inject
+ private BanCommit.Factory banCommitFactory;
+
+ @Override
+ protected void run() throws Failure {
+ try {
+ final BanCommitResult result =
+ banCommitFactory.create().ban(projectControl, commitsToBan, reason);
+
+ final List<ObjectId> newlyBannedCommits =
+ result.getNewlyBannedCommits();
+ if (!newlyBannedCommits.isEmpty()) {
+ stdout.print("The following commits were banned:\n");
+ printCommits(stdout, newlyBannedCommits);
+ }
+
+ final List<ObjectId> alreadyBannedCommits =
+ result.getAlreadyBannedCommits();
+ if (!alreadyBannedCommits.isEmpty()) {
+ stdout.print("The following commits were already banned:\n");
+ printCommits(stdout, alreadyBannedCommits);
+ }
+
+ final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
+ if (!ignoredIds.isEmpty()) {
+ stdout.print("The following ids do not represent commits"
+ + " and were ignored:\n");
+ printCommits(stdout, ignoredIds);
+ }
+ } catch (PermissionDeniedException e) {
+ throw die(e);
+ } catch (IOException e) {
+ throw die(e);
+ } catch (MergeException e) {
+ throw die(e);
+ } catch (InterruptedException e) {
+ throw die(e);
+ } catch (ConcurrentRefUpdateException e) {
+ throw die(e);
+ }
+ }
+
+ private static void printCommits(final PrintWriter stdout,
+ final List<ObjectId> commits) {
+ boolean first = true;
+ for (final ObjectId c : commits) {
+ if (!first) {
+ stdout.print(",\n");
+ }
+ stdout.print(c.getName());
+ first = false;
+ }
+ stdout.print("\n\n");
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 083759cde7..500c84a295 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -14,37 +14,33 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.common.cache.Cache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-
-import java.util.Arrays;
import java.util.SortedSet;
-import java.util.TreeSet;
-abstract class CacheCommand extends BaseCommand {
+abstract class CacheCommand extends SshCommand {
@Inject
- protected EhcachePoolImpl cachePool;
+ protected DynamicMap<Cache<?, ?>> cacheMap;
protected SortedSet<String> cacheNames() {
- final SortedSet<String> names = new TreeSet<String>();
- for (final Ehcache c : getAllCaches()) {
- names.add(c.getName());
+ SortedSet<String> names = Sets.newTreeSet();
+ for (String plugin : cacheMap.plugins()) {
+ for (String name : cacheMap.byPlugin(plugin).keySet()) {
+ names.add(cacheNameOf(plugin, name));
+ }
}
return names;
}
- protected Ehcache[] getAllCaches() {
- final CacheManager cacheMgr = cachePool.getCacheManager();
- final String[] cacheNames = cacheMgr.getCacheNames();
- Arrays.sort(cacheNames);
- final Ehcache[] r = new Ehcache[cacheNames.length];
- for (int i = 0; i < cacheNames.length; i++) {
- r[i] = cacheMgr.getEhcache(cacheNames[i]);
+ protected String cacheNameOf(String plugin, String name) {
+ if ("gerrit".equals(plugin)) {
+ return name;
+ } else {
+ return plugin + "." + name;
}
- return r;
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 38e0bcb7b2..d6fecab80c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,7 +14,9 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -26,12 +28,11 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -45,7 +46,8 @@ import java.util.HashSet;
import java.util.List;
/** Create a new user account. **/
-final class CreateAccountCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+final class CreateAccountCommand extends SshCommand {
@Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
@@ -58,6 +60,9 @@ final class CreateAccountCommand extends BaseCommand {
@Option(name = "--ssh-key", metaVar = "-|KEY", usage = "public key for SSH authentication")
private String sshKey;
+ @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication")
+ private String httpPassword;
+
@Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the user account")
private String username;
@@ -77,24 +82,7 @@ final class CreateAccountCommand extends BaseCommand {
private AccountByEmailCache byEmailCache;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canCreateAccount()) {
- String msg = String.format(
- "fatal: %s does not have \"Create Account\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- createAccount();
- }
- });
- }
-
- private void createAccount() throws OrmException, IOException,
+ protected void run() throws OrmException, IOException,
InvalidSshKeyException, UnloggedFailure {
if (!username.matches(Account.USER_NAME_PATTERN)) {
throw die("Username '" + username + "'"
@@ -108,6 +96,10 @@ final class CreateAccountCommand extends BaseCommand {
new AccountExternalId(id, new AccountExternalId.Key(
AccountExternalId.SCHEME_USERNAME, username));
+ if (httpPassword != null) {
+ extUser.setPassword(httpPassword);
+ }
+
if (db.accountExternalIds().get(extUser.getKey()) != null) {
throw die("username '" + username + "' already exists");
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 98202e28dc..728c20cf0f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -14,15 +14,17 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -34,7 +36,8 @@ import java.util.Set;
* <p>
* Optionally, puts an initial set of user in the newly created group.
*/
-final class CreateGroupCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_GROUP)
+final class CreateGroupCommand extends SshCommand {
@Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
private AccountGroup.Id ownerGroupId;
@@ -65,25 +68,19 @@ final class CreateGroupCommand extends BaseCommand {
private PerformCreateGroup.Factory performCreateGroupFactory;
@Override
- public void start(Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- try {
- performCreateGroupFactory.create().createGroup(groupName,
- groupDescription,
- visibleToAll,
- ownerGroupId,
- initialMembers,
- initialGroups);
- } catch (PermissionDeniedException e) {
- throw die(e);
+ protected void run() throws Failure, OrmException {
+ try {
+ performCreateGroupFactory.create().createGroup(groupName,
+ groupDescription,
+ visibleToAll,
+ ownerGroupId,
+ initialMembers,
+ initialGroups);
+ } catch (PermissionDeniedException e) {
+ throw die(e);
- } catch (NameAlreadyUsedException e) {
- throw die(e);
- }
- }
- });
+ } catch (NameAlreadyUsedException e) {
+ throw die(e);
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index a93cab112e..5f5b1e3b7f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -14,28 +14,27 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.CreateProject;
import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.lib.Constants;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.util.List;
/** Create a new project. **/
-final class CreateProjectCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
+final class CreateProjectCommand extends SshCommand {
@Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
void setProjectNameFromOption(String name) {
if (projectName != null) {
@@ -79,7 +78,7 @@ final class CreateProjectCommand extends BaseCommand {
@Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
+ "(default: master)")
- private String branch = Constants.MASTER;
+ private List<String> branch;
@Option(name = "--empty-commit", usage = "to create initial empty commit")
private boolean createEmptyCommit;
@@ -96,64 +95,45 @@ final class CreateProjectCommand extends BaseCommand {
}
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
private CreateProject.Factory CreateProjectFactory;
@Inject
private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canCreateProject()) {
- String msg =
- String.format(
- "fatal: %s does not have \"Create Project\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ protected void run() throws Exception {
+ try {
+ if (!suggestParent) {
+ if (projectName == null) {
+ throw new UnloggedFailure(1, "fatal: Project name is required.");
}
- PrintWriter p = toPrintWriter(out);
- parseCommandLine();
- try {
- if (!suggestParent) {
- if (projectName == null) {
- throw new UnloggedFailure(1, "fatal: Project name is required.");
- }
- final CreateProjectArgs args = new CreateProjectArgs();
- args.setProjectName(projectName);
- args.ownerIds = ownerIds;
- args.newParent = newParent;
- args.permissionsOnly = permissionsOnly;
- args.projectDescription = projectDescription;
- args.submitType = submitType;
- args.contributorAgreements = contributorAgreements;
- args.signedOffBy = signedOffBy;
- args.contentMerge = contentMerge;
- args.changeIdRequired = requireChangeID;
- args.branch = branch;
- args.createEmptyCommit = createEmptyCommit;
-
- final CreateProject createProject =
- CreateProjectFactory.create(args);
- createProject.createProject();
- } else {
- List<Project.NameKey> parentCandidates =
- suggestParentCandidatesFactory.create().getNameKeys();
-
- for (Project.NameKey parent : parentCandidates) {
- p.print(parent + "\n");
- }
- }
- } catch (ProjectCreationFailedException err) {
- throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
- } finally {
- p.flush();
+ final CreateProjectArgs args = new CreateProjectArgs();
+ args.setProjectName(projectName);
+ args.ownerIds = ownerIds;
+ args.newParent = newParent;
+ args.permissionsOnly = permissionsOnly;
+ args.projectDescription = projectDescription;
+ args.submitType = submitType;
+ args.contributorAgreements = contributorAgreements;
+ args.signedOffBy = signedOffBy;
+ args.contentMerge = contentMerge;
+ args.changeIdRequired = requireChangeID;
+ args.branch = branch;
+ args.createEmptyCommit = createEmptyCommit;
+
+ final CreateProject createProject =
+ CreateProjectFactory.create(args);
+ createProject.createProject();
+ } else {
+ List<Project.NameKey> parentCandidates =
+ suggestParentCandidatesFactory.create().getNameKeys();
+
+ for (Project.NameKey parent : parentCandidates) {
+ stdout.print(parent + "\n");
}
}
- });
+ } catch (ProjectCreationFailedException err) {
+ throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 16461b6b06..2a4dedc6b3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -28,6 +28,7 @@ public class DefaultCommandModule extends CommandModule {
protected void configure() {
final CommandName git = Commands.named("git");
final CommandName gerrit = Commands.named("gerrit");
+ final CommandName plugin = Commands.named(gerrit, "plugin");
// The following commands can be ran on a server in either Master or Slave
// mode. If a command should only be used on a server in one mode, but not
@@ -35,6 +36,7 @@ public class DefaultCommandModule extends CommandModule {
// SlaveCommandModule.
command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
+ command(gerrit, "ban-commit").to(BanCommitCommand.class);
command(gerrit, "flush-caches").to(FlushCaches.class);
command(gerrit, "ls-projects").to(ListProjectsCommand.class);
command(gerrit, "ls-groups").to(ListGroupsCommand.class);
@@ -45,6 +47,15 @@ public class DefaultCommandModule extends CommandModule {
command(gerrit, "stream-events").to(StreamEvents.class);
command(gerrit, "version").to(VersionCommand.class);
+ command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
+ command(plugin, "ls").to(PluginLsCommand.class);
+ command(plugin, "enable").to(PluginEnableCommand.class);
+ command(plugin, "install").to(PluginInstallCommand.class);
+ command(plugin, "reload").to(PluginReloadCommand.class);
+ command(plugin, "remove").to(PluginRemoveCommand.class);
+ command(plugin, "add").to(Commands.key(plugin, "install"));
+ command(plugin, "rm").to(Commands.key(plugin, "remove"));
+
command(git).toProvider(new DispatchCommandProvider(git));
command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
command(git, "upload-pack").to(Upload.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 639cc42b21..fa63041d0c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,21 +14,23 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.cache.Cache;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.sshd.BaseCommand;
import com.google.inject.Inject;
+import com.google.inject.Provider;
-import net.sf.ehcache.Ehcache;
-
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.SortedSet;
/** Causes the caches to purge all entries and reload. */
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
final class FlushCaches extends CacheCommand {
private static final String WEB_SESSIONS = "web_sessions";
@@ -44,27 +46,8 @@ final class FlushCaches extends CacheCommand {
@Inject
IdentifiedUser currentUser;
- private PrintWriter p;
-
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canFlushCaches()) {
- String msg = String.format(
- "fatal: %s does not have \"Flush Caches\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- flush();
- }
- });
- }
-
- private void flush() throws Failure {
+ protected void run() throws Failure {
if (caches.contains(WEB_SESSIONS)
&& !currentUser.getCapabilities().canAdministrateServer()) {
String msg = String.format(
@@ -73,7 +56,6 @@ final class FlushCaches extends CacheCommand {
throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
}
- p = toPrintWriter(err);
if (list) {
if (all || caches.size() > 0) {
throw error("error: cannot use --list with --all or --cache");
@@ -106,26 +88,29 @@ final class FlushCaches extends CacheCommand {
private void doList() {
for (final String name : cacheNames()) {
- p.print(name);
- p.print('\n');
+ stderr.print(name);
+ stderr.print('\n');
}
- p.flush();
+ stderr.flush();
}
private void doBulkFlush() {
try {
- for (final Ehcache c : getAllCaches()) {
- final String name = c.getName();
- if (flush(name)) {
- try {
- c.removeAll();
- } catch (Throwable e) {
- p.println("error: cannot flush cache \"" + name + "\": " + e);
+ for (String plugin : cacheMap.plugins()) {
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin(plugin).entrySet()) {
+ String n = cacheNameOf(plugin, entry.getKey());
+ if (flush(n)) {
+ try {
+ entry.getValue().get().invalidateAll();
+ } catch (Throwable err) {
+ stderr.println("error: cannot flush cache \"" + n + "\": " + err);
+ }
}
}
}
} finally {
- p.flush();
+ stderr.flush();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 69018afbb1..83e88e55b0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -14,25 +14,24 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Task;
import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
-import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;
/** Kill a task in the work queue. */
-final class KillCommand extends BaseCommand {
- @Inject
- private IdentifiedUser currentUser;
-
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.KILL_TASK)
+final class KillCommand extends SshCommand {
@Inject
private WorkQueue workQueue;
@@ -48,33 +47,14 @@ final class KillCommand extends BaseCommand {
}
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canKillTask()) {
- String msg = String.format(
- "fatal: %s does not have \"Kill Task\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- KillCommand.this.commitMurder();
- }
- });
- }
-
- private void commitMurder() {
- final PrintWriter p = toPrintWriter(err);
+ protected void run() {
for (final Integer id : taskIds) {
final Task<?> task = workQueue.getTask(id);
if (task != null) {
task.cancel(true);
} else {
- p.print("kill: " + IdGenerator.format(id) + ": No such task\n");
+ stderr.print("kill: " + IdGenerator.format(id) + ": No such task\n");
}
}
- p.flush();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index e0b988e9c6..f8856f2ddc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -14,27 +14,27 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.data.GroupList;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.client.KeyUtil;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-public class ListGroupsCommand extends BaseCommand {
+public class ListGroupsCommand extends SshCommand {
+ @Inject
+ private GroupCache groupCache;
@Inject
private VisibleGroups.Factory visibleGroupsFactory;
@@ -56,19 +56,14 @@ public class ListGroupsCommand extends BaseCommand {
usage = "user for which the groups should be listed")
private Account.Id user;
- @Override
- public void start(final Environment env) throws IOException {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- ListGroupsCommand.this.display();
- }
- });
- }
+ @Option(name = "--verbose", aliases = {"-v"},
+ usage = "verbose output format with tab-separated columns for the " +
+ "group name, UUID, description, type, owner group name, " +
+ "owner group UUID, and whether the group is visible to all")
+ private boolean verboseOutput;
- private void display() throws Failure {
- final PrintWriter stdout = toPrintWriter(out);
+ @Override
+ protected void run() throws Failure {
try {
if (user != null && !projects.isEmpty()) {
throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
@@ -85,15 +80,27 @@ public class ListGroupsCommand extends BaseCommand {
} else {
groupList = visibleGroups.get();
}
- for (final GroupDetail groupDetail : groupList.getGroups()) {
- stdout.print(groupDetail.group.getName() + "\n");
+
+ final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
+ for (final AccountGroup g : groupList.getGroups()) {
+ formatter.addColumn(g.getName());
+ if (verboseOutput) {
+ formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
+ formatter.addColumn(
+ g.getDescription() != null ? g.getDescription() : "");
+ formatter.addColumn(g.getType().toString());
+ final AccountGroup owningGroup =
+ groupCache.get(g.getOwnerGroupUUID());
+ formatter.addColumn(
+ owningGroup != null ? owningGroup.getName() : "n/a");
+ formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
+ formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+ }
+ formatter.nextLine();
}
- } catch (OrmException e) {
- throw die(e);
+ formatter.finish();
} catch (NoSuchGroupException e) {
throw die(e);
- } finally {
- stdout.flush();
}
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d0bbc72079..13e3f176bb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -30,11 +30,13 @@ final class ListProjectsCommand extends BaseCommand {
@Override
public void run() throws Exception {
parseCommandLine(impl);
- if (impl.isShowTree() && (impl.getShowBranch() != null)) {
- throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
- }
- if (impl.isShowTree() && impl.isShowDescription()) {
- throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+ if (!impl.getFormat().isJson()) {
+ if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+ throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+ }
+ if (impl.isShowTree() && impl.isShowDescription()) {
+ throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+ }
}
impl.display(out);
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 34f64dae0e..90bc07e831 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -31,10 +31,12 @@ public class MasterCommandModule extends CommandModule {
command(gerrit, "rename-group").to(RenameGroupCommand.class);
command(gerrit, "create-project").to(CreateProjectCommand.class);
command(gerrit, "gsql").to(AdminQueryShell.class);
+ command(gerrit, "test-submit-rule").to(TestSubmitRule.class);
command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
command(gerrit, "receive-pack").to(Receive.class);
- command(gerrit, "replicate").to(Replicate.class);
command(gerrit, "set-project-parent").to(AdminSetParent.class);
command(gerrit, "review").to(ReviewCommand.class);
+ command(gerrit, "set-account").to(SetAccountCommand.class);
+ command(gerrit, "set-project").to(SetProjectCommand.class);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
new file mode 100644
index 0000000000..4df3aeeb3e
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -0,0 +1,48 @@
+// 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.sshd.commands;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginEnableCommand extends SshCommand {
+ @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
+ List<String> names;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ if (names != null && !names.isEmpty()) {
+ try {
+ loader.enablePlugins(Sets.newHashSet(names));
+ } catch (PluginInstallException e) {
+ e.printStackTrace(stderr);
+ throw die("plugin failed to enable");
+ }
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
new file mode 100644
index 0000000000..12722ec133
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -0,0 +1,103 @@
+// 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.sshd.commands;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginInstallCommand extends SshCommand {
+ @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
+ private String name;
+
+ @Option(name = "-")
+ void useInput(boolean on) {
+ source = "-";
+ }
+
+ @Argument(index = 0, metaVar = "-|URL", usage = "JAR to load")
+ private String source;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ if (Strings.isNullOrEmpty(source)) {
+ throw die("Argument \"-|URL\" is required");
+ }
+ if (Strings.isNullOrEmpty(name) && "-".equalsIgnoreCase(source)) {
+ throw die("--name required when source is stdin");
+ }
+
+ if (Strings.isNullOrEmpty(name)) {
+ int s = source.lastIndexOf('/');
+ if (0 <= s) {
+ name = source.substring(s + 1);
+ } else {
+ name = source;
+ }
+ }
+
+ InputStream data;
+ if ("-".equalsIgnoreCase(source)) {
+ data = in;
+ } else if (new File(source).isFile()
+ && source.equals(new File(source).getAbsolutePath())) {
+ try {
+ data = new FileInputStream(new File(source));
+ } catch (FileNotFoundException e) {
+ throw die("cannot read " + source);
+ }
+ } else {
+ try {
+ data = new URL(source).openStream();
+ } catch (MalformedURLException e) {
+ throw die("invalid url " + source);
+ } catch (IOException e) {
+ throw die("cannot read " + source);
+ }
+ }
+ try {
+ loader.installPluginFromStream(name, data);
+ } catch (IOException e) {
+ throw die("cannot install plugin");
+ } catch (PluginInstallException e) {
+ e.printStackTrace(stderr);
+ throw die("plugin failed to install");
+ } finally {
+ try {
+ data.close();
+ } catch (IOException err) {
+ }
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
new file mode 100644
index 0000000000..6d7490fb03
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -0,0 +1,42 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginLsCommand extends BaseCommand {
+ @Inject
+ private ListPlugins impl;
+
+ @Override
+ public void start(Environment env) throws IOException {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine(impl);
+ impl.display(out);
+ }
+ });
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
new file mode 100644
index 0000000000..d2429a9320
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -0,0 +1,51 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginReloadCommand extends SshCommand {
+ @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
+ private List<String> names;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ if (names == null || names.isEmpty()) {
+ loader.rescan();
+ } else {
+ try {
+ loader.reload(names);
+ } catch (InvalidPluginException e) {
+ throw die(e.getMessage());
+ } catch (PluginInstallException e) {
+ throw die(e.getMessage());
+ }
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
new file mode 100644
index 0000000000..8baab77661
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -0,0 +1,42 @@
+// 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.sshd.commands;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginRemoveCommand extends SshCommand {
+ @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
+ List<String> names;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() {
+ if (names != null && !names.isEmpty()) {
+ loader.disablePlugins(Sets.newHashSet(names));
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index d9a1c3f551..63680f868e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -15,16 +15,15 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.server.query.change.QueryProcessor;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.util.List;
-class Query extends BaseCommand {
+class Query extends SshCommand {
@Inject
private QueryProcessor processor;
@@ -71,23 +70,23 @@ class Query extends BaseCommand {
processor.setIncludeDependencies(on);
}
+ @Option(name = "--submit-records", usage = "Include submit and label status")
+ void setSubmitRecords(boolean on) {
+ processor.setIncludeSubmitRecords(on);
+ }
+
@Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
private List<String> query;
@Override
- public void start(Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
- parseCommandLine();
- verifyCommandLine();
- processor.query(join(query, " "));
- }
- });
+ protected void run() throws Exception {
+ processor.query(join(query, " "));
}
- private void verifyCommandLine() throws UnloggedFailure {
+ @Override
+ protected void parseCommandLine() throws UnloggedFailure {
+ processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
+ super.parseCommandLine();
if (processor.getIncludeFiles() &&
!(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
throw new UnloggedFailure(1, "--files option needs --patch-sets or --current-patch-set");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 85f53bfe82..b4de75b2d3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -30,9 +30,10 @@ import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
import org.eclipse.jgit.transport.ReceivePack;
import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -41,6 +42,8 @@ import java.util.Set;
/** Receives change upload over SSH using the Git receive-pack protocol. */
final class Receive extends AbstractGitCommand {
+ private static final Logger log = LoggerFactory.getLogger(Receive.class);
+
@Inject
private AsyncReceiveCommits.Factory factory;
@@ -98,9 +101,17 @@ final class Receive extends AbstractGitCommand {
// is larger than the receive.maxObjectSizeLimit gerrit.config parameter
// we want to present this error to the user
if (badStream.getCause() instanceof TooLargeObjectInPackException) {
- PrintWriter p = toPrintWriter(err);
- p.print("error: " + badStream.getCause().getMessage() + "\n");
- p.flush();
+ StringBuilder msg = new StringBuilder();
+ msg.append("Receive error on project \""
+ + projectControl.getProject().getName() + "\"");
+ msg.append(" (user ");
+ msg.append(currentUser.getAccount().getUserName());
+ msg.append(" account ");
+ msg.append(currentUser.getAccountId());
+ msg.append("): ");
+ msg.append(badStream.getCause().getMessage());
+ log.info(msg.toString());
+ throw new UnloggedFailure(128, "error: " + badStream.getCause().getMessage());
}
// This may have been triggered by branch level access controls.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 5b6cf392be..b9abc9237a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -14,20 +14,17 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
-import java.io.IOException;
-
-public class RenameGroupCommand extends BaseCommand {
-
+public class RenameGroupCommand extends SshCommand {
@Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
private String groupName;
@@ -38,21 +35,17 @@ public class RenameGroupCommand extends BaseCommand {
private PerformRenameGroup.Factory performRenameGroupFactory;
@Override
- public void start(final Environment env) throws IOException {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- try {
- performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
- } catch (OrmException e) {
- throw die(e);
- } catch (NameAlreadyUsedException e) {
- throw die(e);
- } catch (NoSuchGroupException e) {
- throw die(e);
- }
- }
- });
+ protected void run() throws Failure {
+ try {
+ performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
+ } catch (OrmException e) {
+ throw die(e);
+ } catch (InvalidNameException e) {
+ throw die(e);
+ } catch (NameAlreadyUsedException e) {
+ throw die(e);
+ } catch (NoSuchGroupException e) {
+ throw die(e);
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
deleted file mode 100644
index bc4e0bba17..0000000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// 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.gerrit.sshd.commands;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.PushAllProjectsOp;
-import com.google.gerrit.server.git.ReplicationQueue;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.sshd.BaseCommand;
-import com.google.inject.Inject;
-
-import org.apache.sshd.server.Environment;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** Force a project to replicate, again. */
-final class Replicate extends BaseCommand {
- @Option(name = "--all", usage = "push all known projects")
- private boolean all;
-
- @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
- private String urlMatch;
-
- @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name")
- private List<String> projectNames = new ArrayList<String>(2);
-
- @Inject
- IdentifiedUser currentUser;
-
- @Inject
- private PushAllProjectsOp.Factory pushAllOpFactory;
-
- @Inject
- private ReplicationQueue replication;
-
- @Inject
- private ProjectCache projectCache;
-
- @Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canStartReplication()) {
- String msg = String.format(
- "fatal: %s does not have \"Start Replication\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- Replicate.this.schedule();
- }
- });
- }
-
- private void schedule() throws Failure {
- if (all && projectNames.size() > 0) {
- throw new Failure(1, "error: cannot combine --all and PROJECT");
- }
-
- if (!replication.isEnabled()) {
- throw new Failure(1, "error: replication not enabled");
- }
-
- if (all) {
- pushAllOpFactory.create(urlMatch).start(0, TimeUnit.SECONDS);
-
- } else {
- for (final String name : projectNames) {
- final Project.NameKey key = new Project.NameKey(name);
- if (projectCache.get(key) != null) {
- replication.scheduleFullSync(key, urlMatch);
- } else {
- throw new Failure(1, "error: '" + name + "': not a Gerrit project");
- }
- }
- }
- }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 640adbf70c..5ebb6c7fbd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -33,13 +33,14 @@ import com.google.gerrit.server.patch.PublishComments;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
+import com.google.inject.Provider;
-import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -52,7 +53,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
-public class ReviewCommand extends BaseCommand {
+public class ReviewCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(ReviewCommand.class);
@@ -67,7 +68,8 @@ public class ReviewCommand extends BaseCommand {
private final Set<PatchSet.Id> patchSetIds = new HashSet<PatchSet.Id>();
- @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "patch to review")
+ @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}",
+ usage = "list of commits or patch sets to review")
void addPatchSetId(final String token) {
try {
patchSetIds.addAll(parsePatchSetId(token));
@@ -78,29 +80,29 @@ public class ReviewCommand extends BaseCommand {
}
}
- @Option(name = "--project", aliases = "-p", usage = "project containing the patch set")
+ @Option(name = "--project", aliases = "-p", usage = "project containing the specified patch set(s)")
private ProjectControl projectControl;
- @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change", metaVar = "MESSAGE")
+ @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE")
private String changeComment;
- @Option(name = "--abandon", usage = "abandon the patch set")
+ @Option(name = "--abandon", usage = "abandon the specified change(s)")
private boolean abandonChange;
- @Option(name = "--restore", usage = "restore an abandoned the patch set")
+ @Option(name = "--restore", usage = "restore the specified abandoned change(s)")
private boolean restoreChange;
- @Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
+ @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
private boolean submitChange;
@Option(name = "--force-message", usage = "publish the message, "
- + "even if the label score cannot be applied due to change being closed")
+ + "even if the label score cannot be applied due to the change being closed")
private boolean forceMessage = false;
- @Option(name = "--publish", usage = "publish a draft patch set")
+ @Option(name = "--publish", usage = "publish the specified draft patch set(s)")
private boolean publishPatchSet;
- @Option(name = "--delete", usage = "delete a draft patch set")
+ @Option(name = "--delete", usage = "delete the specified draft patch set(s)")
private boolean deleteDraftPatchSet;
@Inject
@@ -113,7 +115,7 @@ public class ReviewCommand extends BaseCommand {
private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
@Inject
- private AbandonChange.Factory abandonChangeFactory;
+ private Provider<AbandonChange> abandonChangeProvider;
@Inject
private PublishComments.Factory publishCommentsFactory;
@@ -122,7 +124,7 @@ public class ReviewCommand extends BaseCommand {
private PublishDraft.Factory publishDraftFactory;
@Inject
- private RestoreChange.Factory restoreChangeFactory;
+ private Provider<RestoreChange> restoreChangeProvider;
@Inject
private Submit.Factory submitFactory;
@@ -130,67 +132,60 @@ public class ReviewCommand extends BaseCommand {
private List<ApproveOption> optionList;
@Override
- public final void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- initOptionList();
- parseCommandLine();
- if (abandonChange) {
- if (restoreChange) {
- throw error("abandon and restore actions are mutually exclusive");
- }
- if (submitChange) {
- throw error("abandon and submit actions are mutually exclusive");
- }
- if (publishPatchSet) {
- throw error("abandon and publish actions are mutually exclusive");
- }
- if (deleteDraftPatchSet) {
- throw error("abandon and delete actions are mutually exclusive");
- }
- }
- if (publishPatchSet) {
- if (restoreChange) {
- throw error("publish and restore actions are mutually exclusive");
- }
- if (submitChange) {
- throw error("publish and submit actions are mutually exclusive");
- }
- if (deleteDraftPatchSet) {
- throw error("publish and delete actions are mutually exclusive");
- }
- }
-
- boolean ok = true;
- for (final PatchSet.Id patchSetId : patchSetIds) {
- try {
- approveOne(patchSetId);
- } catch (UnloggedFailure e) {
- ok = false;
- writeError("error: " + e.getMessage() + "\n");
- } catch (NoSuchChangeException e) {
- ok = false;
- writeError("no such change " + patchSetId.getParentKey().get());
- } catch (Exception e) {
- ok = false;
- writeError("fatal: internal server error while approving "
- + patchSetId + "\n");
- log.error("internal error while approving " + patchSetId, e);
- }
- }
-
- if (!ok) {
- throw new UnloggedFailure(1, "one or more approvals failed;"
- + " review output above");
- }
+ protected void run() throws UnloggedFailure {
+ if (abandonChange) {
+ if (restoreChange) {
+ throw error("abandon and restore actions are mutually exclusive");
+ }
+ if (submitChange) {
+ throw error("abandon and submit actions are mutually exclusive");
+ }
+ if (publishPatchSet) {
+ throw error("abandon and publish actions are mutually exclusive");
+ }
+ if (deleteDraftPatchSet) {
+ throw error("abandon and delete actions are mutually exclusive");
+ }
+ }
+ if (publishPatchSet) {
+ if (restoreChange) {
+ throw error("publish and restore actions are mutually exclusive");
+ }
+ if (submitChange) {
+ throw error("publish and submit actions are mutually exclusive");
+ }
+ if (deleteDraftPatchSet) {
+ throw error("publish and delete actions are mutually exclusive");
+ }
+ }
+ boolean ok = true;
+ for (final PatchSet.Id patchSetId : patchSetIds) {
+ try {
+ approveOne(patchSetId);
+ } catch (UnloggedFailure e) {
+ ok = false;
+ writeError("error: " + e.getMessage() + "\n");
+ } catch (NoSuchChangeException e) {
+ ok = false;
+ writeError("no such change " + patchSetId.getParentKey().get());
+ } catch (Exception e) {
+ ok = false;
+ writeError("fatal: internal server error while approving "
+ + patchSetId + "\n");
+ log.error("internal error while approving " + patchSetId, e);
}
- });
+ }
+
+ if (!ok) {
+ throw new UnloggedFailure(1, "one or more approvals failed;"
+ + " review output above");
+ }
}
- private void approveOne(final PatchSet.Id patchSetId) throws
- NoSuchChangeException, OrmException, EmailException, Failure {
+ private void approveOne(final PatchSet.Id patchSetId)
+ throws NoSuchChangeException, OrmException, EmailException, Failure,
+ RepositoryNotFoundException, IOException {
if (changeComment == null) {
changeComment = "";
@@ -208,12 +203,16 @@ public class ReviewCommand extends BaseCommand {
publishCommentsFactory.create(patchSetId, changeComment, aps, forceMessage).call();
if (abandonChange) {
- final ReviewResult result = abandonChangeFactory.create(
- patchSetId, changeComment).call();
+ final AbandonChange abandonChange = abandonChangeProvider.get();
+ abandonChange.setChangeId(patchSetId.getParentKey());
+ abandonChange.setMessage(changeComment);
+ final ReviewResult result = abandonChange.call();
handleReviewResultErrors(result);
} else if (restoreChange) {
- final ReviewResult result = restoreChangeFactory.create(
- patchSetId, changeComment).call();
+ final RestoreChange restoreChange = restoreChangeProvider.get();
+ restoreChange.setChangeId(patchSetId.getParentKey());
+ restoreChange.setMessage(changeComment);
+ final ReviewResult result = restoreChange.call();
handleReviewResultErrors(result);
}
if (submitChange) {
@@ -255,6 +254,9 @@ public class ReviewCommand extends BaseCommand {
case CHANGE_IS_CLOSED:
errMsg += "change is closed";
break;
+ case CHANGE_NOT_ABANDONED:
+ errMsg += "change is not abandoned";
+ break;
case PUBLISH_NOT_PERMITTED:
errMsg += "not permitted to publish change";
break;
@@ -265,11 +267,14 @@ public class ReviewCommand extends BaseCommand {
errMsg += "rule error";
break;
case NOT_A_DRAFT:
- errMsg += "change is not a draft";
+ errMsg += "change/patch set is not a draft";
break;
case GIT_ERROR:
errMsg += "error writing change to git repository";
break;
+ case DEST_BRANCH_NOT_FOUND:
+ errMsg += "destination branch not found";
+ break;
default:
errMsg += "failure in review";
}
@@ -344,7 +349,8 @@ public class ReviewCommand extends BaseCommand {
return projectControl.getProject().getNameKey().equals(change.getProject());
}
- private void initOptionList() {
+ @Override
+ protected void parseCommandLine() throws UnloggedFailure {
optionList = new ArrayList<ApproveOption>();
for (ApprovalType type : approvalTypes.getApprovalTypes()) {
@@ -360,6 +366,8 @@ public class ReviewCommand extends BaseCommand {
"--" + category.getName().toLowerCase().replace(' ', '-');
optionList.add(new ApproveOption(name, usage, type));
}
+
+ super.parseCommandLine();
}
private void writeError(final String msg) {
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
new file mode 100644
index 0000000000..9940fc827c
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -0,0 +1,290 @@
+// 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.sshd.commands;
+
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Set a user's account settings. **/
+final class SetAccountCommand extends BaseCommand {
+
+ @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
+ private Account.Id id;
+
+ @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
+ private String fullName;
+
+ @Option(name = "--active", usage = "set account's state to active")
+ private boolean active;
+
+ @Option(name = "--inactive", usage = "set account's state to inactive")
+ private boolean inactive;
+
+ @Option(name = "--add-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to add to the account")
+ private List<String> addEmails = new ArrayList<String>();
+
+ @Option(name = "--delete-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to delete from the account")
+ private List<String> deleteEmails = new ArrayList<String>();
+
+ @Option(name = "--add-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to add to the account")
+ private List<String> addSshKeys = new ArrayList<String>();
+
+ @Option(name = "--delete-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to delete from the account")
+ private List<String> deleteSshKeys = new ArrayList<String>();
+
+ @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication for the account")
+ private String httpPassword;
+
+ @Inject
+ private IdentifiedUser currentUser;
+
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ private AccountManager manager;
+
+ @Inject
+ private SshKeyCache sshKeyCache;
+
+ @Inject
+ private AccountCache byIdCache;
+
+ @Inject
+ private Realm realm;
+
+ @Override
+ public void start(final Environment env) {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ if (!currentUser.getCapabilities().canAdministrateServer()) {
+ String msg =
+ String.format(
+ "fatal: %s does not have \"Administrator\" capability.",
+ currentUser.getUserName());
+ throw new UnloggedFailure(1, msg);
+ }
+ parseCommandLine();
+ validate();
+ setAccount();
+ }
+ });
+ }
+
+ private void validate() throws UnloggedFailure {
+ if (active && inactive) {
+ throw new UnloggedFailure(1,
+ "--active and --inactive options are mutually exclusive.");
+ }
+ if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
+ throw new UnloggedFailure(1, "Only one option may use the stdin");
+ }
+ if (deleteSshKeys.contains("ALL")) {
+ deleteSshKeys = Collections.singletonList("ALL");
+ }
+ if (deleteEmails.contains("ALL")) {
+ deleteEmails = Collections.singletonList("ALL");
+ }
+ }
+
+ private void setAccount() throws OrmException, IOException, UnloggedFailure {
+
+ final Account account = db.accounts().get(id);
+ boolean accountUpdated = false;
+ boolean sshKeysUpdated = false;
+
+ for (String email : addEmails) {
+ link(id, email);
+ }
+
+ for (String email : deleteEmails) {
+ deleteMail(id, email);
+ }
+
+ if (fullName != null) {
+ if (realm.allowsEdit(FieldName.FULL_NAME)) {
+ account.setFullName(fullName);
+ } else {
+ throw new UnloggedFailure(1, "The realm doesn't allow editing names");
+ }
+ }
+
+ if (httpPassword != null) {
+ setHttpPassword(id, httpPassword);
+ }
+
+ if (active) {
+ accountUpdated = true;
+ account.setActive(true);
+ } else if (inactive) {
+ accountUpdated = true;
+ account.setActive(false);
+ }
+
+ addSshKeys = readSshKey(addSshKeys);
+ if (!addSshKeys.isEmpty()) {
+ sshKeysUpdated = true;
+ addSshKeys(addSshKeys, account);
+ }
+
+ deleteSshKeys = readSshKey(deleteSshKeys);
+ if (!deleteSshKeys.isEmpty()) {
+ sshKeysUpdated = true;
+ deleteSshKeys(deleteSshKeys, account);
+ }
+
+ if (accountUpdated) {
+ db.accounts().update(Collections.singleton(account));
+ byIdCache.evict(id);
+ }
+
+ if (sshKeysUpdated) {
+ sshKeyCache.evict(account.getUserName());
+ }
+ }
+
+ private void addSshKeys(final List<String> keys, final Account account)
+ throws OrmException, UnloggedFailure {
+ List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+ int seq = db.accountSshKeys().byAccount(account.getId()).toList().size();
+ for (String key : keys) {
+ try {
+ seq++;
+ AccountSshKey accountSshKey = sshKeyCache.create(
+ new AccountSshKey.Id(account.getId(), seq), key.trim());
+ accountKeys.add(accountSshKey);
+ } catch (InvalidSshKeyException e) {
+ throw new UnloggedFailure(1, "fatal: invalid ssh key");
+ }
+ }
+ db.accountSshKeys().insert(accountKeys);
+ }
+
+ private void deleteSshKeys(final List<String> keys, final Account account)
+ throws OrmException {
+ ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
+ if (keys.contains("ALL")) {
+ db.accountSshKeys().delete(allKeys);
+ } else {
+ List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+ for (String key : keys) {
+ for (AccountSshKey accountSshKey : allKeys) {
+ if (key.trim().equals(accountSshKey.getSshPublicKey())
+ || accountSshKey.getComment().trim().equals(key)) {
+ accountKeys.add(accountSshKey);
+ }
+ }
+ }
+ db.accountSshKeys().delete(accountKeys);
+ }
+ }
+
+ private void deleteMail(Account.Id id, final String mailAddress)
+ throws UnloggedFailure, OrmException {
+ if (mailAddress.equals("ALL")) {
+ ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+ for (AccountExternalId extId : ids) {
+ if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) {
+ unlink(id, extId.getEmailAddress());
+ }
+ }
+ } else {
+ AccountExternalId.Key key = new AccountExternalId.Key(
+ AccountExternalId.SCHEME_MAILTO, mailAddress);
+ AccountExternalId extId = db.accountExternalIds().get(key);
+ if (extId != null) {
+ unlink(id, mailAddress);
+ }
+ }
+ }
+
+ private void setHttpPassword(Account.Id id, final String httpPassword)
+ throws UnloggedFailure, OrmException {
+ ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+ for (AccountExternalId extId: ids) {
+ if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
+ extId.setPassword(httpPassword);
+ db.accountExternalIds().update(Collections.singleton(extId));
+ byIdCache.evict(id);
+ }
+ }
+ }
+
+ private void unlink(Account.Id id, final String mailAddress)
+ throws UnloggedFailure {
+ try {
+ manager.unlink(id, AuthRequest.forEmail(mailAddress));
+ } catch (AccountException ex) {
+ throw die(ex.getMessage());
+ }
+ }
+
+ private void link(Account.Id id, final String mailAddress)
+ throws UnloggedFailure {
+ try {
+ manager.link(id, AuthRequest.forEmail(mailAddress));
+ } catch (AccountException ex) {
+ throw die(ex.getMessage());
+ }
+ }
+
+ private List<String> readSshKey(final List<String> sshKeys)
+ throws UnsupportedEncodingException, IOException {
+ if (!sshKeys.isEmpty()) {
+ String sshKey = "";
+ int idx = sshKeys.indexOf("-");
+ if (idx >= 0) {
+ sshKey = "";
+ BufferedReader br =
+ new BufferedReader(new InputStreamReader(in, "UTF-8"));
+ String line;
+ while ((line = br.readLine()) != null) {
+ sshKey += line + "\n";
+ }
+ sshKeys.set(idx, sshKey);
+ }
+ }
+ return sshKeys;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
new file mode 100644
index 0000000000..9143f5b1de
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -0,0 +1,171 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.State;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class SetProjectCommand extends SshCommand {
+ private static final Logger log = LoggerFactory
+ .getLogger(SetProjectCommand.class);
+
+ @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project")
+ private ProjectControl projectControl;
+
+ @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of project")
+ private String projectDescription;
+
+ @Option(name = "--submit-type", aliases = {"-t"}, usage = "project submit type\n"
+ + "(default: MERGE_IF_NECESSARY)")
+ private SubmitType submitType;
+
+ @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
+ private Boolean contributorAgreements;
+
+ @Option(name = "--no-contributor-agreements", aliases = {"--nca"}, usage = "if contributor agreement is not required")
+ private Boolean noContributorAgreements;
+
+ @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
+ private Boolean signedOffBy;
+
+ @Option(name = "--no-signed-off-by", aliases = {"--nso"}, usage = "if signed-off-by is not required")
+ private Boolean noSignedOffBy;
+
+ @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
+ private Boolean contentMerge;
+
+ @Option(name = "--no-content-merge", usage = "don't allow automatic conflict resolving within files")
+ private Boolean noContentMerge;
+
+ @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
+ private Boolean requireChangeID;
+
+ @Option(name = "--no-change-id", aliases = {"--nid"}, usage = "if change-id is not required")
+ private Boolean noRequireChangeID;
+
+ @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
+ private State state;
+
+ @Inject
+ private MetaDataUpdate.User metaDataUpdateFactory;
+
+ @Inject
+ private ProjectCache projectCache;
+
+ @Override
+ protected void run() throws Failure {
+ validate();
+ Project ctlProject = projectControl.getProject();
+ Project.NameKey nameKey = ctlProject.getNameKey();
+ String name = ctlProject.getName();
+ final StringBuilder err = new StringBuilder();
+
+ try {
+ MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
+ try {
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
+
+ project.setRequireChangeID(requireChangeID != null ? requireChangeID
+ : project.isRequireChangeID());
+
+ project.setRequireChangeID(noRequireChangeID != null
+ ? !noRequireChangeID : project.isRequireChangeID());
+
+ project.setSubmitType(submitType != null ? submitType : project
+ .getSubmitType());
+
+ project.setUseContentMerge(contentMerge != null ? contentMerge
+ : project.isUseContentMerge());
+
+ project.setUseContentMerge(noContentMerge != null ? !noContentMerge
+ : project.isUseContentMerge());
+
+ project.setUseContributorAgreements(contributorAgreements != null
+ ? contributorAgreements : project.isUseContributorAgreements());
+
+ project.setUseContributorAgreements(noContributorAgreements != null
+ ? !noContributorAgreements : project.isUseContributorAgreements());
+
+ project.setUseSignedOffBy(signedOffBy != null ? signedOffBy : project
+ .isUseSignedOffBy());
+
+ project.setUseContentMerge(noSignedOffBy != null ? !noSignedOffBy
+ : project.isUseContentMerge());
+
+ project.setDescription(projectDescription != null ? projectDescription
+ : project.getDescription());
+
+ project.setState(state != null ? state : project.getState());
+
+ md.setMessage("Project settings updated");
+ config.commit(md);
+ } finally {
+ md.close();
+ }
+ } catch (RepositoryNotFoundException notFound) {
+ err.append("error: Project " + name + " not found\n");
+ } catch (IOException e) {
+ final String msg = "Cannot update project " + name;
+ log.error(msg, e);
+ err.append("error: " + msg + "\n");
+ } catch (ConfigInvalidException e) {
+ final String msg = "Cannot update project " + name;
+ log.error(msg, e);
+ err.append("error: " + msg + "\n");
+ }
+ projectCache.evict(ctlProject);
+
+ if (err.length() > 0) {
+ while (err.charAt(err.length() - 1) == '\n') {
+ err.setLength(err.length() - 1);
+ }
+ throw new UnloggedFailure(1, err.toString());
+ }
+ }
+
+ private void validate() throws UnloggedFailure {
+ checkExclusivity(contentMerge, "--use-content-merge",
+ noContentMerge, "--no-content-merge");
+
+ checkExclusivity(contributorAgreements, "--use-contributor-agreements",
+ noContributorAgreements, "--no-contributor-agreements");
+
+ checkExclusivity(signedOffBy, "--use-signed-off-by",
+ noSignedOffBy, "--no-signed-off-by");
+
+ checkExclusivity(requireChangeID, "--require-change-id",
+ noRequireChangeID, "--no-change-id");
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 6e1a32ba04..f873824a19 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -25,12 +25,11 @@ import com.google.gerrit.server.patch.RemoveReviewer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -43,7 +42,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
-public class SetReviewersCommand extends BaseCommand {
+public class SetReviewersCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(SetReviewersCommand.class);
@@ -85,28 +84,21 @@ public class SetReviewersCommand extends BaseCommand {
private Set<Change.Id> changes = new HashSet<Change.Id>();
@Override
- public final void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- parseCommandLine();
-
- boolean ok = true;
- for (Change.Id changeId : changes) {
- try {
- ok &= modifyOne(changeId);
- } catch (Exception err) {
- ok = false;
- log.error("Error updating reviewers on change " + changeId, err);
- writeError("fatal", "internal error while updating " + changeId);
- }
- }
-
- if (!ok) {
- throw error("fatal: one or more updates failed; review output above");
- }
+ protected void run() throws UnloggedFailure {
+ boolean ok = true;
+ for (Change.Id changeId : changes) {
+ try {
+ ok &= modifyOne(changeId);
+ } catch (Exception err) {
+ ok = false;
+ log.error("Error updating reviewers on change " + changeId, err);
+ writeError("fatal", "internal error while updating " + changeId);
}
- });
+ }
+
+ if (!ok) {
+ throw error("fatal: one or more updates failed; review output above");
+ }
}
private boolean modifyOne(Change.Id changeId) throws Exception {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 4de10d6312..bdcb4fb24f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -14,19 +14,20 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.h2.H2CacheImpl;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.sshd.BaseCommand;
import com.google.gerrit.sshd.SshDaemon;
import com.google.inject.Inject;
-
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.config.CacheConfiguration;
+import com.google.inject.Provider;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
@@ -36,7 +37,6 @@ import org.kohsuke.args4j.Option;
import java.io.File;
import java.io.IOException;
-import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
@@ -45,8 +45,11 @@ import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
+import java.util.Map;
+import java.util.SortedMap;
/** Show the current cache states. */
+@RequiresCapability(GlobalCapability.VIEW_CACHES)
final class ShowCaches extends CacheCommand {
private static volatile long serverStarted;
@@ -68,9 +71,6 @@ final class ShowCaches extends CacheCommand {
private boolean showJVM;
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
private WorkQueue workQueue;
@Inject
@@ -80,98 +80,81 @@ final class ShowCaches extends CacheCommand {
@SitePath
private File sitePath;
- private PrintWriter p;
+ @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
+ private int columns = 80;
+ private int nw;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canViewCaches()) {
- String msg = String.format(
- "fatal: %s does not have \"View Caches\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- display();
+ public void start(Environment env) throws IOException {
+ String s = env.getEnv().get(Environment.ENV_COLUMNS);
+ if (s != null && !s.isEmpty()) {
+ try {
+ columns = Integer.parseInt(s);
+ } catch (NumberFormatException err) {
+ columns = 80;
}
- });
+ }
+ super.start(env);
}
- private void display() {
- p = toPrintWriter(out);
-
+ @Override
+ protected void run() {
+ nw = columns - 50;
Date now = new Date();
- p.format(
+ stdout.format(
"%-25s %-20s now %16s\n",
"Gerrit Code Review",
Version.getVersion() != null ? Version.getVersion() : "",
new SimpleDateFormat("HH:mm:ss zzz").format(now));
- p.format(
+ stdout.format(
"%-25s %-20s uptime %16s\n",
"", "",
uptime(now.getTime() - serverStarted));
- p.print('\n');
+ stdout.print('\n');
- p.print(String.format(//
- "%1s %-18s %-4s|%-20s| %-5s |%-14s|\n" //
+ stdout.print(String.format(//
+ "%1s %-"+nw+"s|%-21s| %-5s |%-9s|\n" //
, "" //
, "Name" //
- , "Max" //
- , "Object Count" //
+ , "Entries" //
, "AvgGet" //
, "Hit Ratio" //
));
- p.print(String.format(//
- "%1s %-18s %-4s|%6s %6s %6s| %-5s |%-4s %-4s %-4s|\n" //
+ stdout.print(String.format(//
+ "%1s %-"+nw+"s|%6s %6s %7s| %-5s |%-4s %-4s|\n" //
, "" //
, "" //
- , "Age" //
- , "Disk" //
, "Mem" //
- , "Cnt" //
- , "" //
, "Disk" //
+ , "Space" //
+ , "" //
, "Mem" //
- , "Agg" //
+ , "Disk" //
));
- p.print("------------------"
- + "-------+--------------------+----------+--------------+\n");
- for (final Ehcache cache : getAllCaches()) {
- final CacheConfiguration cfg = cache.getCacheConfiguration();
- final boolean useDisk = cfg.isDiskPersistent() || cfg.isOverflowToDisk();
- final Statistics stat = cache.getStatistics();
- final long total = stat.getCacheHits() + stat.getCacheMisses();
-
- if (useDisk) {
- p.print(String.format(//
- "D %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
- , cache.getName() //
- , interval(cfg.getTimeToLiveSeconds()) //
- , count(stat.getDiskStoreObjectCount()) //
- , count(stat.getMemoryStoreObjectCount()) //
- , count(stat.getObjectCount()) //
- , duration(stat.getAverageGetTime()) //
- , percent(stat.getOnDiskHits(), total) //
- , percent(stat.getInMemoryHits(), total) //
- , percent(stat.getCacheHits(), total) //
- ));
- } else {
- p.print(String.format(//
- " %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
- , cache.getName() //
- , interval(cfg.getTimeToLiveSeconds()) //
- , "", "" //
- , count(stat.getObjectCount()) //
- , duration(stat.getAverageGetTime()) //
- , "", "" //
- , percent(stat.getCacheHits(), total) //
- ));
- }
+ stdout.print("--");
+ for (int i = 0; i < nw; i++) {
+ stdout.print('-');
+ }
+ stdout.print("+---------------------+---------+---------+\n");
+
+ Map<String, H2CacheImpl<?, ?>> disks = Maps.newTreeMap();
+ printMemoryCaches(disks, sortedCoreCaches());
+ printMemoryCaches(disks, sortedPluginCaches());
+ for (Map.Entry<String, H2CacheImpl<?, ?>> entry : disks.entrySet()) {
+ H2CacheImpl<?, ?> cache = entry.getValue();
+ CacheStats stat = cache.stats();
+ H2CacheImpl.DiskStats disk = cache.diskStats();
+ stdout.print(String.format(
+ "D %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+ entry.getKey(),
+ count(cache.size()),
+ count(disk.size()),
+ bytes(disk.space()),
+ duration(stat.averageLoadPenalty()),
+ percent(stat.hitCount(), stat.requestCount()),
+ percent(disk.hitCount(), disk.requestCount())));
}
- p.print('\n');
+ stdout.print('\n');
if (gc) {
System.gc();
@@ -187,7 +170,52 @@ final class ShowCaches extends CacheCommand {
jvmSummary();
}
- p.flush();
+ stdout.flush();
+ }
+
+ private void printMemoryCaches(
+ Map<String, H2CacheImpl<?, ?>> disks,
+ Map<String, Cache<?,?>> caches) {
+ for (Map.Entry<String, Cache<?,?>> entry : caches.entrySet()) {
+ Cache<?,?> cache = entry.getValue();
+ if (cache instanceof H2CacheImpl) {
+ disks.put(entry.getKey(), (H2CacheImpl<?,?>)cache);
+ continue;
+ }
+ CacheStats stat = cache.stats();
+ stdout.print(String.format(
+ " %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+ entry.getKey(),
+ count(cache.size()),
+ "",
+ "",
+ duration(stat.averageLoadPenalty()),
+ percent(stat.hitCount(), stat.requestCount()),
+ ""));
+ }
+ }
+
+ private Map<String, Cache<?, ?>> sortedCoreCaches() {
+ SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin("gerrit").entrySet()) {
+ m.put(cacheNameOf("gerrit", entry.getKey()), entry.getValue().get());
+ }
+ return m;
+ }
+
+ private Map<String, Cache<?, ?>> sortedPluginCaches() {
+ SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+ for (String plugin : cacheMap.plugins()) {
+ if ("gerrit".equals(plugin)) {
+ continue;
+ }
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin(plugin).entrySet()) {
+ m.put(cacheNameOf(plugin, entry.getKey()), entry.getValue().get());
+ }
+ }
+ return m;
}
private void memSummary() {
@@ -200,17 +228,17 @@ final class ShowCaches extends CacheCommand {
final int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
- p.format("Mem: %s total = %s used + %s free + %s buffers\n",
+ stdout.format("Mem: %s total = %s used + %s free + %s buffers\n",
bytes(mTotal),
bytes(mInuse - jgitBytes),
bytes(mFree),
bytes(jgitBytes));
- p.format(" %s max\n", bytes(mMax));
- p.format(" %8d open files, %8d cpus available, %8d threads\n",
+ stdout.format(" %s max\n", bytes(mMax));
+ stdout.format(" %8d open files, %8d cpus available, %8d threads\n",
jgitOpen,
r.availableProcessors(),
ManagementFactory.getThreadMXBean().getThreadCount());
- p.print('\n');
+ stdout.print('\n');
}
private void taskSummary() {
@@ -224,7 +252,7 @@ final class ShowCaches extends CacheCommand {
case SLEEPING: tasksSleeping++; break;
}
}
- p.format(
+ stdout.format(
"Tasks: %4d total = %4d running + %4d ready + %4d sleeping\n",
tasksTotal,
tasksRunning,
@@ -245,7 +273,7 @@ final class ShowCaches extends CacheCommand {
oldest = Math.min(oldest, s.getCreationTime());
}
- p.format(
+ stdout.format(
"SSH: %4d users, oldest session started %s ago\n",
list.size(),
uptime(now - oldest));
@@ -254,22 +282,22 @@ final class ShowCaches extends CacheCommand {
private void jvmSummary() {
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
- p.format("JVM: %s %s %s\n",
+ stdout.format("JVM: %s %s %s\n",
runtimeBean.getVmVendor(),
runtimeBean.getVmName(),
runtimeBean.getVmVersion());
- p.format(" on %s %s %s\n", "",
+ stdout.format(" on %s %s %s\n", "",
osBean.getName(),
osBean.getVersion(),
osBean.getArch());
try {
- p.format(" running as %s on %s\n",
+ stdout.format(" running as %s on %s\n",
System.getProperty("user.name"),
InetAddress.getLocalHost().getHostName());
} catch (UnknownHostException e) {
}
- p.format(" cwd %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
- p.format(" site %s\n", path(sitePath));
+ stdout.format(" cwd %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
+ stdout.format(" site %s\n", path(sitePath));
}
private String path(File file) {
@@ -325,45 +353,24 @@ final class ShowCaches extends CacheCommand {
return String.format("%6d", cnt);
}
- private String duration(double ms) {
- if (Math.abs(ms) <= 0.05) {
+ private String duration(double ns) {
+ if (ns < 0.5) {
return "";
}
- String suffix = "ms";
- if (ms >= 1000) {
- ms /= 1000;
- suffix = "s ";
+ String suffix = "ns";
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
+ suffix = "us";
}
- return String.format("%4.1f%s", ms, suffix);
- }
-
- private String interval(double ttl) {
- if (ttl == 0) {
- return "inf";
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
+ suffix = "ms";
}
-
- String suffix = "s";
- if (ttl >= 60) {
- ttl /= 60;
- suffix = "m";
-
- if (ttl >= 60) {
- ttl /= 60;
- suffix = "h";
- }
-
- if (ttl >= 24) {
- ttl /= 24;
- suffix = "d";
-
- if (ttl >= 365) {
- ttl /= 365;
- suffix = "y";
- }
- }
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
+ suffix = "s ";
}
-
- return Integer.toString((int) ttl) + suffix;
+ return String.format("%4.1f%s", ns, suffix);
}
private String percent(final long value, final long total) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index a72ce90518..a1a5b8f369 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -14,21 +14,21 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.sshd.SshDaemon;
import com.google.gerrit.sshd.SshSession;
import com.google.inject.Inject;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
-import org.apache.sshd.server.Environment;
import org.apache.sshd.server.session.ServerSession;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -40,39 +40,16 @@ import java.util.Date;
import java.util.List;
/** Show the current SSH connections. */
-final class ShowConnections extends BaseCommand {
+@RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
+final class ShowConnections extends SshCommand {
@Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
private boolean numeric;
- private PrintWriter p;
-
- @Inject
- IdentifiedUser currentUser;
-
@Inject
private SshDaemon daemon;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canViewConnections()) {
- String msg = String.format(
- "fatal: %s does not have \"View Connections\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- ShowConnections.this.display();
- }
- });
- }
-
- private void display() throws Failure {
- p = toPrintWriter(out);
-
+ protected void run() throws Failure {
final IoAcceptor acceptor = daemon.getIoAcceptor();
if (acceptor == null) {
throw new Failure(1, "fatal: sshd no longer running");
@@ -93,9 +70,9 @@ final class ShowConnections extends BaseCommand {
});
final long now = System.currentTimeMillis();
- p.print(String.format("%-8s %8s %8s %-15s %s\n", //
+ stdout.print(String.format("%-8s %8s %8s %-15s %s\n", //
"Session", "Start", "Idle", "User", "Remote Host"));
- p.print("--------------------------------------------------------------\n");
+ stdout.print("--------------------------------------------------------------\n");
for (final IoSession io : list) {
ServerSession s = (ServerSession) ServerSession.getSession(io, true);
SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
@@ -104,16 +81,14 @@ final class ShowConnections extends BaseCommand {
final long start = io.getCreationTime();
final long idle = now - io.getLastIoTime();
- p.print(String.format("%8s %8s %8s %-15.15s %.30s\n", //
+ stdout.print(String.format("%8s %8s %8s %-15.15s %.30s\n", //
id(sd), //
time(now, start), //
age(idle), //
username(sd), //
hostname(remoteAddress)));
}
- p.print("--\n");
-
- p.flush();
+ stdout.print("--\n");
}
private static String id(final SshSession sd) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index e835ffe6bf..f862484a4a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,13 +23,13 @@ import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
+import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
@@ -39,7 +39,7 @@ import java.util.concurrent.TimeUnit;
/** Display the current work queue. */
@AdminHighPriorityCommand
-final class ShowQueue extends BaseCommand {
+final class ShowQueue extends SshCommand {
@Option(name = "-w", usage = "display without line width truncation")
private boolean wide;
@@ -52,12 +52,11 @@ final class ShowQueue extends BaseCommand {
@Inject
private IdentifiedUser currentUser;
- private PrintWriter p;
private int columns = 80;
private int taskNameWidth;
@Override
- public void start(final Environment env) {
+ public void start(final Environment env) throws IOException {
String s = env.getEnv().get(Environment.ENV_COLUMNS);
if (s != null && !s.isEmpty()) {
try {
@@ -66,19 +65,11 @@ final class ShowQueue extends BaseCommand {
columns = 80;
}
}
-
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- ShowQueue.this.display();
- }
- });
+ super.start(env);
}
- private void display() {
- p = toPrintWriter(out);
-
+ @Override
+ protected void run() {
final List<Task<?>> pending = workQueue.getTasks();
Collections.sort(pending, new Comparator<Task<?>>() {
public int compare(Task<?> a, Task<?> b) {
@@ -103,9 +94,9 @@ final class ShowQueue extends BaseCommand {
taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 8 - 4;
- p.print(String.format("%-8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%-8s %-12s %-8s %s\n", //
"Task", "State", "", "Command"));
- p.print("----------------------------------------------"
+ stdout.print("----------------------------------------------"
+ "--------------------------------\n");
int numberOfPendingTasks = 0;
@@ -158,7 +149,7 @@ final class ShowQueue extends BaseCommand {
// Shows information about tasks depending on the user rights
if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
- p.print(String.format("%8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%8s %-12s %-8s %s\n", //
id(task.getTaskId()), start, "", format(task)));
} else if (regularUserCanSee) {
if (remoteName == null) {
@@ -167,20 +158,18 @@ final class ShowQueue extends BaseCommand {
remoteName = remoteName + "/" + projectName;
}
- p.print(String.format("%8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%8s %-12s %-8s %s\n", //
id(task.getTaskId()), start, "", remoteName));
}
}
- p.print("----------------------------------------------"
+ stdout.print("----------------------------------------------"
+ "--------------------------------\n");
if (viewAll) {
numberOfPendingTasks = pending.size();
}
- p.print(" " + numberOfPendingTasks + " tasks\n");
-
- p.flush();
+ stdout.print(" " + numberOfPendingTasks + " tasks\n");
}
private static String id(final int id) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
index 32ab2db09e..0e1a1feb01 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
@@ -27,11 +27,14 @@ public class SlaveCommandModule extends CommandModule {
command(gerrit, "approve").to(ErrorSlaveMode.class);
command(gerrit, "create-account").to(ErrorSlaveMode.class);
+ command(gerrit, "create-group").to(ErrorSlaveMode.class);
command(gerrit, "create-project").to(ErrorSlaveMode.class);
command(gerrit, "gsql").to(ErrorSlaveMode.class);
command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
+ command(gerrit, "rename-group").to(ErrorSlaveMode.class);
command(gerrit, "replicate").to(ErrorSlaveMode.class);
command(gerrit, "review").to(ErrorSlaveMode.class);
command(gerrit, "set-project-parent").to(ErrorSlaveMode.class);
+ command(gerrit, "set-reviewers").to(ErrorSlaveMode.class);
}
}
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 ff6dea962d..ff6dea962d 100755..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
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
new file mode 100644
index 0000000000..c8544e42da
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
@@ -0,0 +1,241 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.SubmitRecord;
+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.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.events.SubmitLabelAttribute;
+import com.google.gerrit.server.events.SubmitRecordAttribute;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.InputStreamReader;
+import java.io.PushbackReader;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/** Command that allows testing of prolog submit-rules in a live instance. */
+final class TestSubmitRule extends SshCommand {
+ @Inject
+ private ReviewDb db;
+
+ @Inject
+ private PrologEnvironment.Factory envFactory;
+
+ @Inject
+ private ChangeControl.Factory ccFactory;
+
+ @Inject
+ private AccountCache accountCache;
+
+ final @AnonymousCowardName String anonymousCowardName;
+
+ @Argument(index = 0, required = true, usage = "ChangeId to load in prolog environment")
+ private String changeId;
+
+ @Option(name = "-s",
+ usage = "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
+ private boolean useStdin;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "--no-filters", aliases = {"-n"},
+ usage = "Don't run the submit_filter/2 from the parent projects")
+ private boolean skipSubmitFilters;
+
+ private static final String[] PACKAGE_LIST = {Prolog.BUILTIN, "gerrit"};
+
+ @Inject
+ public TestSubmitRule(@AnonymousCowardName String anonymous) {
+ anonymousCowardName = anonymous;
+ }
+ private PrologMachineCopy newMachine() {
+ BufferingPrologControl ctl = new BufferingPrologControl();
+ ctl.setMaxDatabaseSize(16 * 1024);
+ ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
+ return PrologMachineCopy.save(ctl);
+ }
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ PushbackReader inReader = new PushbackReader(new InputStreamReader(in));
+
+ try {
+ PrologEnvironment pcl;
+
+ List<Change> changeList =
+ db.changes().byKey(new Change.Key(changeId)).toList();
+ if (changeList.size() != 1)
+ throw new UnloggedFailure(1, "Invalid ChangeId");
+
+ Change c = changeList.get(0);
+ PatchSet ps = db.patchSets().get(c.currentPatchSetId());
+ // Will throw exception if current user can not access this change, and
+ // thus will leak information that a change-id is valid even though the
+ // user are not allowed to see the change.
+ // See http://code.google.com/p/gerrit/issues/detail?id=1586
+ ChangeControl cc = ccFactory.controlFor(c);
+ ProjectState projectState = cc.getProjectControl().getProjectState();
+
+ if (useStdin) {
+ pcl = envFactory.create(newMachine());
+ } else {
+ pcl = projectState.newPrologEnvironment();
+ }
+
+ pcl.set(StoredValues.REVIEW_DB, db);
+ pcl.set(StoredValues.CHANGE, c);
+ pcl.set(StoredValues.PATCH_SET, ps);
+ pcl.set(StoredValues.CHANGE_CONTROL, cc);
+ if (useStdin) {
+ pcl.initialize(PACKAGE_LIST);
+ pcl.execute(Prolog.BUILTIN, "consult_stream",
+ SymbolTerm.intern("stdin"), new JavaObjectTerm(inReader));
+ }
+
+ List<Term> results = new ArrayList<Term>();
+ Term submitRule =
+ pcl.once("gerrit", "locate_submit_rule", new VariableTerm());
+
+ for (Term[] template : pcl.all("gerrit", "can_submit", submitRule,
+ new VariableTerm())) {
+ results.add(template[1]);
+ }
+
+ if (!skipSubmitFilters) {
+ runSubmitFilters(projectState, results, pcl);
+ }
+
+ List<SubmitRecord> res = cc.resultsToSubmitRecord(submitRule, results);
+ for (SubmitRecord r : res) {
+ if (format.isJson()) {
+ SubmitRecordAttribute submitRecord = new SubmitRecordAttribute();
+ submitRecord.status = r.status.name();
+
+ List<SubmitLabelAttribute> submitLabels = new LinkedList<SubmitLabelAttribute>();
+ for(SubmitRecord.Label l : r.labels) {
+ SubmitLabelAttribute label = new SubmitLabelAttribute();
+ label.label = l.label;
+ label.status= l.status.name();
+ if(l.appliedBy != null) {
+ Account a = accountCache.get(l.appliedBy).getAccount();
+ label.by = new AccountAttribute();
+ label.by.email = a.getPreferredEmail();
+ label.by.name = a.getFullName();
+ label.by.username = a.getUserName();
+ }
+ submitLabels.add(label);
+ }
+ submitRecord.labels = submitLabels;
+ format.newGson().toJson(submitRecord, new TypeToken<SubmitRecordAttribute>() {}.getType(), stdout);
+ stdout.print('\n');
+ } else {
+ for(SubmitRecord.Label l : r.labels) {
+ stdout.print(l.label + ": " + l.status);
+ if(l.appliedBy != null) {
+ AccountInfo a = new AccountInfo(accountCache.get(l.appliedBy).getAccount());
+ stdout.print(" by " + a.getNameEmail(anonymousCowardName));
+ }
+ stdout.print('\n');
+ }
+ stdout.print("\n" + r.status.name() + "\n");
+ }
+ }
+ } catch (Exception e) {
+ throw new UnloggedFailure("Processing of prolog script failed: " + e);
+ }
+ }
+
+ private void runSubmitFilters(ProjectState projectState, List<Term> results,
+ PrologEnvironment pcl) throws UnloggedFailure {
+ ProjectState parentState = projectState.getParentState();
+ PrologEnvironment childEnv = pcl;
+ Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
+ projectsSeen.add(projectState.getProject().getNameKey());
+
+ while (parentState != null) {
+ if (!projectsSeen.add(parentState.getProject().getNameKey())) {
+ // parent has been seen before, stop walk up inheritance tree
+ break;
+ }
+ PrologEnvironment parentEnv;
+ try {
+ parentEnv = parentState.newPrologEnvironment();
+ } catch (CompileException err) {
+ throw new UnloggedFailure("Cannot consult rules.pl for "
+ + parentState.getProject().getName() + err);
+ }
+
+ parentEnv.copyStoredValues(childEnv);
+ Term filterRule =
+ parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
+ if (filterRule != null) {
+ try {
+ Term resultsTerm = ChangeControl.toListTerm(results);
+ results.clear();
+ Term[] template =
+ parentEnv.once("gerrit", "filter_submit_results", filterRule,
+ resultsTerm, new VariableTerm());
+ @SuppressWarnings("unchecked")
+ final List<? extends Term> termList =
+ ((ListTerm) template[2]).toJava();
+ results.addAll(termList);
+ } catch (PrologException err) {
+ throw new UnloggedFailure("Exception calling " + filterRule + " of "
+ + parentState.getProject().getName() + err);
+ } catch (RuntimeException err) {
+ throw new UnloggedFailure("Exception calling " + filterRule + " of "
+ + parentState.getProject().getName() + err);
+ }
+ }
+
+ parentState = parentState.getParentState();
+ childEnv = parentEnv;
+ }
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 001863b5f2..addbb84fdd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -15,29 +15,16 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.common.Version;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
-import org.apache.sshd.server.Environment;
-
-import java.io.PrintWriter;
-
-final class VersionCommand extends BaseCommand {
+final class VersionCommand extends SshCommand {
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- parseCommandLine();
-
- String v = Version.getVersion();
- if (v == null) {
- throw new Failure(1, "fatal: version unavailable");
- }
+ protected void run() throws Failure {
+ String v = Version.getVersion();
+ if (v == null) {
+ throw new Failure(1, "fatal: version unavailable");
+ }
- final PrintWriter stdout = toPrintWriter(out);
- stdout.println("gerrit version " + v);
- stdout.flush();
- }
- });
+ stdout.println("gerrit version " + v);
}
}
diff --git a/gerrit-util-cli/.gitignore b/gerrit-util-cli/.gitignore
index 194bedcbc4..35069e79da 100644
--- a/gerrit-util-cli/.gitignore
+++ b/gerrit-util-cli/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-cli.iml \ No newline at end of file
diff --git a/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
index c780f4418c..e9441bb123 100644
--- a/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 4ecbda4aee..4886d09881 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index de2f7e98eb..a9f5229fa0 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -46,6 +46,7 @@ import org.kohsuke.args4j.NamedOptionDef;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.BooleanOptionHandler;
+import org.kohsuke.args4j.spi.EnumOptionHandler;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Setter;
@@ -53,9 +54,11 @@ import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
+import java.util.Set;
/**
* Extended command line parser which handles --foo=value arguments.
@@ -66,7 +69,6 @@ import java.util.ResourceBundle;
* args4j style format prior to invoking args4j for parsing.
*/
public class CmdLineParser {
-
public interface Factory {
CmdLineParser create(Object bean);
}
@@ -118,6 +120,67 @@ public class CmdLineParser {
out.write('\n');
}
+ public void printQueryStringUsage(String name, StringWriter out) {
+ out.write(name);
+
+ char next = '?';
+ List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+ for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+ if (handler.option instanceof NamedOptionDef) {
+ NamedOptionDef n = (NamedOptionDef) handler.option;
+
+ if (handler instanceof BooleanOptionHandler) {
+ booleans.add(n);
+ continue;
+ }
+
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ out.write('=');
+
+ String var = handler.getDefaultMetaVariable();
+ if (handler instanceof EnumOptionHandler) {
+ var = var.substring(1, var.length() - 1);
+ var = var.replaceAll(" ", "");
+ }
+ out.write(var);
+ if (!n.required()) {
+ out.write(']');
+ }
+ if (n.isMultiValued()) {
+ out.write('*');
+ }
+ }
+ }
+ for (NamedOptionDef n : booleans) {
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ if (!n.required()) {
+ out.write(']');
+ }
+ }
+ }
+
public boolean wasHelpRequestedByOption() {
return parser.help.value;
}
@@ -148,6 +211,12 @@ public class CmdLineParser {
public void parseOptionMap(Map<String, String[]> parameters)
throws CmdLineException {
+ parseOptionMap(parameters, Collections.<String>emptySet());
+ }
+
+ public void parseOptionMap(Map<String, String[]> parameters,
+ Set<String> argNames)
+ throws CmdLineException {
ArrayList<String> tmp = new ArrayList<String>();
for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
String name = ent.getKey();
@@ -169,7 +238,9 @@ public class CmdLineParser {
}
} else {
for (String value : ent.getValue()) {
- tmp.add(name);
+ if (!argNames.contains(ent.getKey())) {
+ tmp.add(name);
+ }
tmp.add(value);
}
}
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
index 7af544c836..1ea73ccb58 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
@@ -38,7 +38,6 @@ public class OptionHandlerUtil {
return (Key<OptionHandler<T>>) Key.get(handlerType);
}
- @SuppressWarnings("unchecked")
public static <T> Module moduleFor(final Class<T> type, Class<? extends OptionHandler<T>> impl) {
return new FactoryModuleBuilder()
.implement(handlerOf(type), impl)
diff --git a/gerrit-util-ssl/.gitignore b/gerrit-util-ssl/.gitignore
index 194bedcbc4..e552ad516e 100644
--- a/gerrit-util-ssl/.gitignore
+++ b/gerrit-util-ssl/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-ssl.iml \ No newline at end of file
diff --git a/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
index 589908f32c..e9441bb123 100644
--- a/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 2e49d47478..beedb8ff1f 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/.gitignore b/gerrit-war/.gitignore
index 194bedcbc4..dc8c7adc09 100644
--- a/gerrit-war/.gitignore
+++ b/gerrit-war/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs \ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-war.iml \ No newline at end of file
diff --git a/gerrit-war/.settings/org.eclipse.core.resources.prefs b/gerrit-war/.settings/org.eclipse.core.resources.prefs
index d404b00aaf..abdea9ac03 100644
--- a/gerrit-war/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-war/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:37 PDT 2011
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 733d976a87..1f3750ee0b 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
index 233d53dd18..52467a01b4 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -14,7 +14,7 @@
package com.google.gerrit.httpd;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 01b4a44b2e..1a556c2afd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -18,11 +18,12 @@ import static com.google.inject.Scopes.SINGLETON;
import static com.google.inject.Stage.PRODUCTION;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
import com.google.gerrit.httpd.auth.openid.OpenIdModule;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -32,11 +33,12 @@ import com.google.gerrit.server.config.MasterNodeStartup;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
import com.google.gerrit.server.schema.DataSourceProvider;
import com.google.gerrit.server.schema.DatabaseModule;
import com.google.gerrit.server.schema.SchemaModule;
@@ -112,6 +114,11 @@ public class WebAppInitializer extends GuiceServletContextListener {
sshInjector = createSshInjector();
webInjector = createWebInjector();
+ PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
+ env.setCfgInjector(cfgInjector);
+ env.setSshInjector(sshInjector);
+ env.setHttpInjector(webInjector);
+
// Push the Provider<HttpServletRequest> down into the canonical
// URL provider. Its optional for that provider, but since we can
// supply one we should do so, in case the administrator has not
@@ -193,10 +200,11 @@ public class WebAppInitializer extends GuiceServletContextListener {
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
- modules.add(new EhcachePoolImpl.Module());
+ modules.add(new DefaultCacheFactory.Module());
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
- modules.add(new PushReplication.Module());
+ modules.add(new SignedTokenRestTokenVerifier.Module());
+ modules.add(new PluginModule());
modules.add(new CanonicalWebUrlModule() {
@Override
protected Class<? extends Provider<String>> provider() {
@@ -209,18 +217,21 @@ public class WebAppInitializer extends GuiceServletContextListener {
private Injector createSshInjector() {
final List<Module> modules = new ArrayList<Module>();
- modules.add(new SshModule());
+ modules.add(sysInjector.getInstance(SshModule.class));
modules.add(new MasterCommandModule());
return sysInjector.createChildInjector(modules);
}
private Injector createWebInjector() {
final List<Module> modules = new ArrayList<Module>();
+ modules.add(RequestContextFilter.module());
+ modules.add(AllRequestFilter.module());
modules.add(sysInjector.getInstance(GitOverHttpModule.class));
modules.add(sshInjector.getInstance(WebModule.class));
modules.add(sshInjector.getInstance(WebSshGlueModule.class));
modules.add(CacheBasedWebSession.module());
modules.add(HttpContactStoreConnection.module());
+ modules.add(new HttpPluginModule());
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
if (authConfig.getAuthType() == AuthType.OPENID) {
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index 5993790122..45f630e19b 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -48,10 +48,6 @@ log4j.logger.org.openid4java.discovery.Discovery=ERROR
log4j.logger.org.openid4java.server.RealmVerifier=ERROR
log4j.logger.org.openid4java.message.AuthSuccess=ERROR
-# Silence non-critical messages from ehcache
-#
-log4j.logger.net.sf.ehcache=WARN
-
# Silence non-critical messages from c3p0 (if used).
#
log4j.logger.com.mchange.v2.c3p0=WARN
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
index 117bf61fdc..3ae9440e41 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!--
Jetty configuration to place "gerrit.war" into the root context,
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
index 652acadeb9..59cc040678 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!--
Jetty configuration to correctly handle SSL/HTTPS traffic when
diff --git a/pom.xml b/pom.xml
index 27157ff819..5c1303cb22 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.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
<name>Gerrit Code Review - Parent</name>
<url>http://code.google.com/p/gerrit/</url>
@@ -46,11 +46,11 @@ limitations under the License.
</issueManagement>
<properties>
- <jgitVersion>1.3.0.201202151440-r.76-gc3fca97</jgitVersion>
+ <jgitVersion>2.0.0.201206130900-r.24-g170caea</jgitVersion>
<gwtormVersion>1.4</gwtormVersion>
<gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
- <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
- <gwtVersion>2.3.0</gwtVersion>
+ <gwtexpuiVersion>1.2.6</gwtexpuiVersion>
+ <gwtVersion>2.4.0</gwtVersion>
<slf4jVersion>1.6.1</slf4jVersion>
<guiceVersion>3.0</guiceVersion>
<jettyVersion>7.2.1.v20101111</jettyVersion>
@@ -74,7 +74,7 @@ limitations under the License.
<module>gerrit-antlr</module>
<module>gerrit-common</module>
- <module>gerrit-ehcache</module>
+ <module>gerrit-cache-h2</module>
<module>gerrit-httpd</module>
<module>gerrit-launcher</module>
<module>gerrit-main</module>
@@ -87,9 +87,27 @@ limitations under the License.
<module>gerrit-gwtdebug</module>
<module>gerrit-war</module>
+ <module>gerrit-extension-api</module>
+
<module>gerrit-gwtui</module>
</modules>
+ <profiles>
+ <profile>
+ <id>all</id>
+ <modules>
+ <module>gerrit-plugin-api</module>
+ <module>gerrit-plugin-archetype</module>
+ </modules>
+ </profile>
+ <profile>
+ <activation>
+ <activeByDefault>true</activeByDefault>
+ </activation>
+ <id>no-plugins</id>
+ </profile>
+ </profiles>
+
<licenses>
<license>
<name>Apache License, 2.0</name>
@@ -333,7 +351,7 @@ limitations under the License.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
- <version>1.4</version>
+ <version>1.6</version>
</plugin>
<plugin>
@@ -363,7 +381,7 @@ limitations under the License.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>gwt-maven-plugin</artifactId>
- <version>2.3.0</version>
+ <version>2.4.0</version>
</plugin>
<plugin>
@@ -425,6 +443,14 @@ limitations under the License.
</configuration>
</plugin>
</plugins>
+
+ <extensions>
+ <extension>
+ <groupId>net.anzix.aws</groupId>
+ <artifactId>s3-maven-wagon</artifactId>
+ <version>3.2</version>
+ </extension>
+ </extensions>
</build>
<dependencies>
@@ -444,6 +470,18 @@ limitations under the License.
<dependencyManagement>
<dependencies>
<dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>12.0.1</version>
+ </dependency>
+
+ <dependency>
<groupId>gwtorm</groupId>
<artifactId>gwtorm</artifactId>
<version>${gwtormVersion}</version>
@@ -481,9 +519,8 @@ limitations under the License.
<dependency>
<groupId>org.openid4java</groupId>
- <artifactId>openid4java-consumer</artifactId>
- <version>0.9.6</version>
- <type>pom</type>
+ <artifactId>openid4java</artifactId>
+ <version>0.9.8</version>
<exclusions>
<exclusion>
<!-- conflicts with our use of guice 3.0 -->
@@ -518,6 +555,12 @@ limitations under the License.
</dependency>
<dependency>
+ <groupId>org.apache.mina</groupId>
+ <artifactId>mina-core</artifactId>
+ <version>2.0.5</version>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-core</artifactId>
<version>0.5.1-r1095809</version>
@@ -536,12 +579,6 @@ limitations under the License.
</dependency>
<dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache-core</artifactId>
- <version>1.7.2</version>
- </dependency>
-
- <dependency>
<groupId>args4j</groupId>
<artifactId>args4j</artifactId>
<version>2.0.16</version>
@@ -805,23 +842,29 @@ limitations under the License.
<artifactId>PrologCafe</artifactId>
<version>1.3</version>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ <version>1.1.0</version>
+ </dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
- <id>gerrit-maven-repository</id>
- <url>https://gerrit-maven.storage.googleapis.com</url>
+ <id>gerrit-maven</id>
+ <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
</repository>
<repository>
- <id>java.net-repository</id>
- <url>http://download.java.net/maven/2/</url>
+ <id>jgit-repository</id>
+ <url>http://download.eclipse.org/jgit/maven</url>
</repository>
<repository>
- <id>gson</id>
- <url>https://google-gson.googlecode.com/svn/mavenrepo/</url>
+ <id>java.net-repository</id>
+ <url>http://download.java.net/maven/2/</url>
</repository>
<repository>
@@ -833,5 +876,10 @@ limitations under the License.
<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/deploy_api.sh b/tools/deploy_api.sh
new file mode 100755
index 0000000000..e5909e541e
--- /dev/null
+++ b/tools/deploy_api.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+set -e
+
+SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
+VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
+VER=${VER%-sources.jar}
+
+type=release
+case $VER in
+*-SNAPSHOT)
+ echo >&2 "fatal: Cannot deploy $VER"
+ echo >&2 " Use ./tools/version.sh --release && mvn clean package"
+ exit 1
+ ;;
+*-[0-9]*-g*) type=snapshot ;;
+esac
+URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
+
+
+echo "Deploying $type gerrit-extension-api $VER"
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-extension-api \
+ -Dversion=$VER \
+ -Dpackaging=jar \
+ -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-extension-api \
+ -Dversion=$VER \
+ -Dpackaging=java-source \
+ -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
+ -Djava-source=false \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+
+echo "Deploying $type gerrit-plugin-api $VER"
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-plugin-api \
+ -Dversion=$VER \
+ -Dpackaging=jar \
+ -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-plugin-api \
+ -Dversion=$VER \
+ -Dpackaging=java-source \
+ -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
+ -Djava-source=false \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index ea76ee105f..f007da40fa 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -10,6 +10,10 @@
<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-prettify&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtui&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtdebug&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
<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.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
@@ -32,5 +36,5 @@
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;-da:com.google.gwtexpui.globalkey.client.KeyCommandSet&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
</launchConfiguration>
diff --git a/tools/pgm_daemon.launch b/tools/pgm_daemon.launch
index fd7b50fa82..cedf47003d 100644
--- a/tools/pgm_daemon.launch
+++ b/tools/pgm_daemon.launch
@@ -10,6 +10,10 @@
<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
<stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
<stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-war&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-main&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
<booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
diff --git a/tools/release.sh b/tools/release.sh
index 4a872a1bdb..de1835784e 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -25,7 +25,7 @@ then
fi
./tools/version.sh --release &&
-mvn clean package $include_docs
+mvn clean install $include_docs -P all
rc=$?
./tools/version.sh --reset
diff --git a/tools/version.sh b/tools/version.sh
index c3d9417f23..d3e4cd53d4 100755
--- a/tools/version.sh
+++ b/tools/version.sh
@@ -6,7 +6,7 @@
# Java based Maven plugin so its fully portable.
#
-POM_FILES=$(git ls-files | grep pom.xml)
+POM_FILES=$(git ls-files | grep pom.xml | grep -v gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml)
case "$1" in
--snapshot=*)