Skip to content

Commit 3425d78

Browse files
committed
Initial commit
0 parents  commit 3425d78

File tree

10 files changed

+406
-0
lines changed

10 files changed

+406
-0
lines changed

README

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Eager PR Updates
2+
3+
Starting from Bitbucket 7.0+ internal references on repositories aren't generated or updated as often as they were in previous versions.
4+
5+
The issue is reported here: https://jira.atlassian.com/browse/BSERV-12284
6+
7+
As a result, CI tools which use refs/pull-requests/*/from - the references might not be available and fail the build. This can be resolved by calling an API
8+
or viewing the diff of a pull-request.
9+
10+
Instead, this plugin adds a PostReceiveHook at a project or repository scope. If enabled, when a pull-request is opened or the target branch of the pull-request
11+
on the repository is changed then it will call the SDK to get the effectiveDiff of the pull-request. As a result of this, the internal ref is updated and can be used.
12+
13+
Tested on Bitbucket 7.1.1
14+
15+
`atlas-package` to package this plugin, then install it on Bitbucket Server through the UPM.

pom.xml

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<modelVersion>4.0.0</modelVersion>
4+
5+
<groupId>com.cyanoth</groupId>
6+
<artifactId>eagerpr</artifactId>
7+
<version>0.1.1</version>
8+
<packaging>atlassian-plugin</packaging>
9+
10+
<name>Eager PullRequest Internal-Refs Update</name>
11+
<description>Force bitbucket to update internal refs immediately on a pull-request change</description>
12+
13+
<organization>
14+
<name>Cyanoth</name>
15+
<url>https://github.com/Cyanoth</url>
16+
</organization>
17+
18+
<properties>
19+
<maven.compiler.source>1.8</maven.compiler.source>
20+
<maven.compiler.target>1.8</maven.compiler.target>
21+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
22+
23+
<amps.version>8.0.2</amps.version>
24+
<bitbucket.version>7.1.1</bitbucket.version>
25+
<bitbucket.data.version>${bitbucket.version}</bitbucket.data.version>
26+
27+
<!-- This property ensures consistency between the key in atlassian-plugin.xml and the OSGi bundle's key. -->
28+
<atlassian.plugin.key>${project.groupId}.${project.artifactId}</atlassian.plugin.key>
29+
30+
<atlassian.spring.scanner.version>1.2.13</atlassian.spring.scanner.version>
31+
<javax.inject.version>1</javax.inject.version>
32+
<jsr311.version>1.1.1</jsr311.version>
33+
<plugin.testrunner.version>2.0.1</plugin.testrunner.version>
34+
</properties>
35+
36+
<dependencyManagement>
37+
<dependencies>
38+
<dependency>
39+
<groupId>com.atlassian.bitbucket.server</groupId>
40+
<artifactId>bitbucket-parent</artifactId>
41+
<version>${bitbucket.version}</version>
42+
<type>pom</type>
43+
<scope>import</scope>
44+
</dependency>
45+
</dependencies>
46+
</dependencyManagement>
47+
<dependencies>
48+
<dependency>
49+
<groupId>com.atlassian.bitbucket.server</groupId>
50+
<artifactId>bitbucket-api</artifactId>
51+
<scope>provided</scope>
52+
</dependency>
53+
<dependency>
54+
<groupId>com.atlassian.bitbucket.server</groupId>
55+
<artifactId>bitbucket-spi</artifactId>
56+
<scope>provided</scope>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>com.atlassian.plugin</groupId>
61+
<artifactId>atlassian-spring-scanner-annotation</artifactId>
62+
<version>${atlassian.spring.scanner.version}</version>
63+
<scope>provided</scope>
64+
</dependency>
65+
<dependency>
66+
<groupId>com.atlassian.sal</groupId>
67+
<artifactId>sal-api</artifactId>
68+
<scope>provided</scope>
69+
</dependency>
70+
<dependency>
71+
<groupId>com.atlassian.plugin</groupId>
72+
<artifactId>atlassian-spring-scanner-annotation</artifactId>
73+
<version>${atlassian.spring.scanner.version}</version>
74+
<scope>compile</scope>
75+
</dependency>
76+
<dependency>
77+
<groupId>org.springframework</groupId>
78+
<artifactId>spring-context</artifactId>
79+
<scope>provided</scope>
80+
</dependency>
81+
<dependency>
82+
<groupId>com.google.code.gson</groupId>
83+
<artifactId>gson</artifactId>
84+
<scope>provided</scope>
85+
</dependency>
86+
<dependency>
87+
<groupId>javax.inject</groupId>
88+
<artifactId>javax.inject</artifactId>
89+
<version>${javax.inject.version}</version>
90+
<scope>provided</scope>
91+
</dependency>
92+
<dependency>
93+
<groupId>javax.servlet</groupId>
94+
<artifactId>javax.servlet-api</artifactId>
95+
<scope>provided</scope>
96+
</dependency>
97+
<dependency>
98+
<groupId>javax.ws.rs</groupId>
99+
<artifactId>jsr311-api</artifactId>
100+
<version>${jsr311.version}</version>
101+
<scope>provided</scope>
102+
</dependency>
103+
<dependency>
104+
<groupId>org.apache.commons</groupId>
105+
<artifactId>commons-lang3</artifactId>
106+
<scope>provided</scope>
107+
</dependency>
108+
109+
<dependency>
110+
<groupId>com.atlassian.plugins</groupId>
111+
<artifactId>atlassian-plugins-osgi-testrunner</artifactId>
112+
<version>${plugin.testrunner.version}</version>
113+
<scope>test</scope>
114+
</dependency>
115+
<dependency>
116+
<groupId>junit</groupId>
117+
<artifactId>junit</artifactId>
118+
<scope>test</scope>
119+
</dependency>
120+
</dependencies>
121+
122+
<build>
123+
<plugins>
124+
<plugin>
125+
<groupId>com.atlassian.maven.plugins</groupId>
126+
<artifactId>bitbucket-maven-plugin</artifactId>
127+
<version>${amps.version}</version>
128+
<extensions>true</extensions>
129+
<configuration>
130+
<products>
131+
<product>
132+
<id>bitbucket</id>
133+
<instanceId>bitbucket</instanceId>
134+
<version>${bitbucket.version}</version>
135+
<dataVersion>${bitbucket.data.version}</dataVersion>
136+
</product>
137+
</products>
138+
<instructions>
139+
<Atlassian-Plugin-Key>${atlassian.plugin.key}</Atlassian-Plugin-Key>
140+
141+
<!-- Add package to export here -->
142+
<Export-Package>
143+
com.cyanoth.eagerpr.api,
144+
</Export-Package>
145+
146+
<!-- Add package import here -->
147+
<Import-Package>
148+
org.springframework.osgi.*;resolution:="optional",
149+
org.eclipse.gemini.blueprint.*;resolution:="optional",
150+
*
151+
</Import-Package>
152+
153+
<!-- Ensure plugin is Spring powered -->
154+
<Spring-Context>*</Spring-Context>
155+
</instructions>
156+
</configuration>
157+
</plugin>
158+
<plugin>
159+
<groupId>com.atlassian.plugin</groupId>
160+
<artifactId>atlassian-spring-scanner-maven-plugin</artifactId>
161+
<version>${atlassian.spring.scanner.version}</version>
162+
<executions>
163+
<execution>
164+
<goals>
165+
<goal>atlassian-spring-scanner</goal>
166+
</goals>
167+
<phase>process-classes</phase>
168+
</execution>
169+
</executions>
170+
<configuration>
171+
<scannedDependencies>
172+
<dependency>
173+
<groupId>com.atlassian.plugin</groupId>
174+
<artifactId>atlassian-spring-scanner-external-jar</artifactId>
175+
</dependency>
176+
</scannedDependencies>
177+
<verbose>false</verbose>
178+
</configuration>
179+
</plugin>
180+
</plugins>
181+
</build>
182+
</project>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.cyanoth.eagerpr;
2+
3+
import com.atlassian.bitbucket.hook.repository.RepositoryHook;
4+
import com.atlassian.bitbucket.hook.repository.RepositoryHookService;
5+
import com.atlassian.bitbucket.permission.Permission;
6+
import com.atlassian.bitbucket.pull.PullRequest;
7+
import com.atlassian.bitbucket.repository.Repository;
8+
import com.atlassian.bitbucket.scope.RepositoryScope;
9+
import com.atlassian.bitbucket.user.SecurityService;
10+
import com.atlassian.bitbucket.util.Operation;
11+
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
12+
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.stereotype.Component;
17+
import com.atlassian.bitbucket.scm.ScmService;
18+
19+
@Scanned
20+
@Component("InternalPRRefRefresh")
21+
class InternalPRRefRefresh {
22+
private static final Logger log = LoggerFactory.getLogger(InternalPRRefRefresh.class);
23+
24+
private final ScmService scmService;
25+
private final RepositoryHookService repositoryHookService;
26+
private final SecurityService securityService;
27+
28+
@Autowired
29+
InternalPRRefRefresh(@ComponentImport ScmService scmService,
30+
@ComponentImport final RepositoryHookService repositoryHookService,
31+
@ComponentImport final SecurityService securityService) {
32+
this.scmService = scmService;
33+
this.repositoryHookService = repositoryHookService;
34+
this.securityService = securityService;
35+
}
36+
37+
/**
38+
* Make the target repository of a pull-request update its internal refs, if hook is enabled.
39+
* @param pullRequest The pull-request object
40+
*/
41+
void refreshInternalPrRefs(PullRequest pullRequest) {
42+
try {
43+
final long perfStartTime = log.isDebugEnabled() ? System.currentTimeMillis() : 0;
44+
45+
// Only run this on repositories that have the Post-hook enabled
46+
if (!(isHookEnabled(pullRequest.getToRef().getRepository())))
47+
return;
48+
49+
triggerInternalRefUpdate(pullRequest);
50+
51+
final long perfEndtime = log.isDebugEnabled() ? System.currentTimeMillis() : 0;
52+
53+
log.debug("EagerPr: RefreshInternalPRRefresh triggered internal ref update for {} it took: {}",
54+
getPullRequestString(pullRequest),
55+
(perfEndtime - perfStartTime) + "ms");
56+
57+
}
58+
catch (Exception e) { // Catch all here so we don't bubble-up to the event-handler
59+
log.error("EagerPr: RefreshInternalPRRefresh has failed", e) ;
60+
}
61+
}
62+
63+
/**
64+
* Check whether the plugin repository post-receive hook is enabled/
65+
* @param repository The target repository of the pull-request
66+
* @return True - Hook enabled. False otherwise.
67+
* @throws Exception Thrown by securityService, caller must handle.
68+
*/
69+
private boolean isHookEnabled(Repository repository) throws Exception {
70+
return this.securityService.withPermission(Permission.REPO_ADMIN, "Check Post Receive Hook State")
71+
.call((Operation<Boolean, Exception>) () -> {
72+
RepositoryHook hook = repositoryHookService.getByKey(new RepositoryScope(repository), PluginProperties.HOOK_KEY);
73+
return hook != null && hook.isEnabled();
74+
});
75+
}
76+
77+
/**
78+
* Make Bitbucket update the internal pull-request refs
79+
* @param pullRequest The pull-request object
80+
*/
81+
private void triggerInternalRefUpdate(PullRequest pullRequest) {
82+
// Retrieving the effective diff forces the pull request refs to be updated (Source, same call from Bitbucket 7.1.1 Sourcecode)
83+
scmService.getPullRequestCommandFactory(pullRequest).effectiveDiff().call();
84+
}
85+
86+
/**
87+
* @param pullRequest The pull-request object
88+
* @return Friendly string with information about the pull-request
89+
*/
90+
private String getPullRequestString(PullRequest pullRequest) {
91+
return String.format("pull-request id: %d repository: %s/%s",
92+
pullRequest.getId(),
93+
pullRequest.getToRef().getRepository().getProject().getKey(),
94+
pullRequest.getToRef().getRepository().getSlug());
95+
}
96+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.cyanoth.eagerpr;
2+
3+
public final class PluginProperties {
4+
public static final String HOOK_KEY = "com.cyanoth.eagerpr:eagerpr-hook";
5+
6+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.cyanoth.eagerpr;
2+
3+
import com.atlassian.bitbucket.hook.repository.PostRepositoryHook;
4+
import com.atlassian.bitbucket.hook.repository.PostRepositoryHookContext;
5+
import com.atlassian.bitbucket.hook.repository.RepositoryHookRequest;
6+
7+
import javax.annotation.Nonnull;
8+
9+
public class PostReceiveHook implements PostRepositoryHook<RepositoryHookRequest> {
10+
11+
@Override
12+
public void postUpdate(@Nonnull PostRepositoryHookContext context,
13+
@Nonnull RepositoryHookRequest hookRequest) {
14+
// No Behaviour
15+
}
16+
17+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.cyanoth.eagerpr;
2+
3+
import com.atlassian.bitbucket.event.pull.PullRequestOpenedEvent;
4+
import com.atlassian.bitbucket.event.pull.PullRequestReopenedEvent;
5+
import com.atlassian.bitbucket.event.pull.PullRequestRescopedEvent;
6+
import com.atlassian.event.api.EventListener;
7+
import com.atlassian.event.api.EventPublisher;
8+
import com.atlassian.plugin.spring.scanner.annotation.export.ExportAsService;
9+
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
10+
import org.springframework.beans.factory.DisposableBean;
11+
import org.springframework.beans.factory.InitializingBean;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import com.atlassian.plugin.spring.scanner.annotation.component.Scanned;
14+
import org.springframework.stereotype.Component;
15+
16+
@Scanned
17+
@ExportAsService(PullRequestEventsListener.class)
18+
@Component("PullRequestEventsListener")
19+
public class PullRequestEventsListener implements InitializingBean, DisposableBean {
20+
21+
private final EventPublisher eventPublisher;
22+
private final InternalPRRefRefresh internalPRRefRefresh;
23+
24+
@Autowired
25+
PullRequestEventsListener(@ComponentImport EventPublisher eventPublisher,
26+
InternalPRRefRefresh internalPRRefRefresh) {
27+
this.eventPublisher = eventPublisher;
28+
this.internalPRRefRefresh = internalPRRefRefresh;
29+
}
30+
31+
@Override
32+
public void afterPropertiesSet() {
33+
eventPublisher.register(this);
34+
}
35+
36+
@Override
37+
public void destroy() {
38+
eventPublisher.unregister(this);
39+
}
40+
41+
@EventListener
42+
public void pullRequestOpenedEvent(PullRequestOpenedEvent event) {
43+
internalPRRefRefresh.refreshInternalPrRefs(event.getPullRequest());
44+
}
45+
46+
@EventListener
47+
public void pullRequestReopenedEvent(PullRequestReopenedEvent event) {
48+
internalPRRefRefresh.refreshInternalPrRefs(event.getPullRequest());
49+
}
50+
51+
@EventListener
52+
public void pullRequestRescopedEvent(PullRequestRescopedEvent event) {
53+
// Only care if the from (source) branch of the pull-request has updated since that affects refs/pull-requests/**/from
54+
// Changes to the to (target) branch won't affect the hash of refs/pull-requests/**/from so it would be a waste of resources doing anything.
55+
if (event.isFromHashUpdated())
56+
internalPRRefRefresh.refreshInternalPrRefs(event.getPullRequest());
57+
}
58+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<beans xmlns="http://www.springframework.org/schema/beans"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:atlassian-scanner="http://www.atlassian.com/schema/atlassian-scanner"
5+
xsi:schemaLocation="http://www.springframework.org/schema/beans
6+
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
7+
http://www.atlassian.com/schema/atlassian-scanner
8+
http://www.atlassian.com/schema/atlassian-scanner/atlassian-scanner.xsd">
9+
<atlassian-scanner:scan-indexes/>
10+
</beans>

0 commit comments

Comments
 (0)