Skip to content

Commit ee50db5

Browse files
author
jicheng
committed
v1.3.0: 性能优化 - 优先读取本地缓存分支
- 新增 useQuickFetch 选项控制分支获取策略 - 优先读取本地缓存的分支信息(瞬间响应) - 保留按提交时间排序的核心功能 - 无工作空间时回退到 ls-remote 或 clone - 移除可能阻塞的网络操作,避免页面卡死
1 parent 3dc486d commit ee50db5

5 files changed

Lines changed: 274 additions & 69 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
<groupId>io.jenkins.plugins</groupId>
1313
<artifactId>active-git-branches-plugin</artifactId>
14-
<version>1.1.1</version>
14+
<version>1.3.0</version>
1515
<packaging>hpi</packaging>
1616

1717
<name>Active Git Branches Parameter</name>

src/main/java/io/jenkins/plugins/activegitbranches/ActiveGitBranchesParameterDefinition.java

Lines changed: 252 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
88
import edu.umd.cs.findbugs.annotations.NonNull;
99
import hudson.Extension;
10+
import hudson.FilePath;
11+
import hudson.model.AbstractProject;
1012
import hudson.model.Item;
13+
import hudson.model.Job;
1114
import hudson.model.ParameterDefinition;
1215
import hudson.model.ParameterValue;
1316
import hudson.security.ACL;
@@ -26,11 +29,13 @@
2629
import org.kohsuke.stapler.DataBoundConstructor;
2730
import org.kohsuke.stapler.DataBoundSetter;
2831
import org.kohsuke.stapler.QueryParameter;
32+
import org.kohsuke.stapler.Stapler;
2933
import org.kohsuke.stapler.StaplerRequest;
3034

3135
import java.io.File;
3236
import java.io.IOException;
3337
import java.util.*;
38+
import java.util.Collections;
3439
import java.util.logging.Level;
3540
import java.util.logging.Logger;
3641
import java.util.regex.Pattern;
@@ -53,6 +58,7 @@ public class ActiveGitBranchesParameterDefinition extends ParameterDefinition {
5358
private String branchFilter;
5459
private String alwaysIncludeBranches;
5560
private String defaultValue;
61+
private boolean useQuickFetch = true;
5662

5763
@DataBoundConstructor
5864
public ActiveGitBranchesParameterDefinition(String name, String repositoryUrl, int maxBranchCount, String description) {
@@ -105,6 +111,15 @@ public void setDefaultValue(String defaultValue) {
105111
this.defaultValue = defaultValue;
106112
}
107113

114+
public boolean isUseQuickFetch() {
115+
return useQuickFetch;
116+
}
117+
118+
@DataBoundSetter
119+
public void setUseQuickFetch(boolean useQuickFetch) {
120+
this.useQuickFetch = useQuickFetch;
121+
}
122+
108123
@Override
109124
public ParameterValue createValue(StaplerRequest req, JSONObject jo) {
110125
String value = jo.getString("value");
@@ -145,18 +160,218 @@ public List<BranchInfo> fetchBranches() {
145160

146161
/**
147162
* Fetches branches from the remote Git repository.
148-
* This method throws exceptions on failure.
163+
* Strategy:
164+
* 1. Try workspace fetch + for-each-ref (fast + time-sorted) - PREFERRED
165+
* 2. If no workspace: use ls-remote (fast, alphabetical) or clone (slow, time-sorted)
149166
*/
150167
private List<BranchInfo> fetchBranchesInternal() throws IOException, InterruptedException {
151-
final List<BranchInfo> branchInfos = new ArrayList<>();
152-
153168
if (repositoryUrl == null || repositoryUrl.isEmpty()) {
154169
throw new IOException("Repository URL is not configured");
155170
}
156171

172+
// First, always try workspace-based fetch (fast + preserves time sorting)
173+
List<BranchInfo> result = tryFetchFromWorkspace();
174+
if (result != null) {
175+
LOGGER.info("Fetched branches from workspace with time-based sorting");
176+
return result;
177+
}
178+
179+
// No workspace available, fall back based on useQuickFetch setting
180+
if (useQuickFetch) {
181+
// Fast but no time sorting
182+
LOGGER.info("No workspace available, using ls-remote (alphabetical sort)");
183+
return fetchBranchesQuick();
184+
} else {
185+
// Slow but preserves time sorting
186+
LOGGER.info("No workspace available, using clone (time-based sort)");
187+
return fetchBranchesWithFullClone();
188+
}
189+
}
190+
191+
/**
192+
* Quick fetch using git ls-remote (no clone required).
193+
* This is much faster but doesn't provide commit timestamps for sorting.
194+
* Branches are sorted alphabetically instead.
195+
*/
196+
private List<BranchInfo> fetchBranchesQuick() throws IOException, InterruptedException {
197+
final List<BranchInfo> branchInfos = new ArrayList<>();
198+
157199
StandardCredentials credentials = getCredentials();
200+
File tempDir = createTempDirectory();
201+
202+
try {
203+
TaskListener listener = new LogTaskListener(LOGGER, Level.INFO);
204+
EnvVars env = new EnvVars();
205+
206+
GitClient git = Git.with(listener, env)
207+
.in(tempDir)
208+
.using("jgit")
209+
.getClient();
210+
211+
if (credentials != null) {
212+
git.addCredentials(repositoryUrl, credentials);
213+
}
214+
215+
// Use ls-remote to get remote references without cloning
216+
Map<String, org.eclipse.jgit.lib.ObjectId> remoteRefs = git.getRemoteReferences(
217+
repositoryUrl, null, true, false);
218+
219+
for (Map.Entry<String, org.eclipse.jgit.lib.ObjectId> entry : remoteRefs.entrySet()) {
220+
String refName = entry.getKey();
221+
222+
// Filter for branches (refs/heads/...)
223+
if (refName.startsWith("refs/heads/")) {
224+
String branchName = refName.substring("refs/heads/".length());
225+
226+
// Apply branch filter
227+
boolean isAlwaysIncluded = matchesAlwaysInclude(branchName);
228+
229+
if (!isAlwaysIncluded && !matchesBranchFilter(branchName)) {
230+
continue;
231+
}
232+
233+
// Use 0 as commit time since ls-remote doesn't provide it
234+
// Branches will be sorted alphabetically instead
235+
branchInfos.add(new BranchInfo(branchName, 0L));
236+
}
237+
}
238+
} finally {
239+
deleteDirectory(tempDir);
240+
}
241+
242+
// Sort alphabetically (since we don't have commit times)
243+
branchInfos.sort((a, b) -> a.getName().compareToIgnoreCase(b.getName()));
244+
245+
return applyLimits(branchInfos);
246+
}
247+
248+
/**
249+
* Try to fetch branches from existing Job workspace.
250+
* Uses git fetch + for-each-ref which is faster than cloning.
251+
* Returns null if workspace is not available.
252+
*/
253+
private List<BranchInfo> tryFetchFromWorkspace() {
254+
try {
255+
// Get the current Job from Stapler request context
256+
StaplerRequest request = Stapler.getCurrentRequest();
257+
if (request == null) {
258+
LOGGER.fine("No Stapler request context available");
259+
return null;
260+
}
261+
262+
Job<?, ?> job = request.findAncestorObject(Job.class);
263+
if (job == null) {
264+
LOGGER.fine("No Job found in request context");
265+
return null;
266+
}
267+
268+
// Get workspace
269+
FilePath workspace = null;
270+
if (job instanceof AbstractProject) {
271+
workspace = ((AbstractProject<?, ?>) job).getSomeWorkspace();
272+
}
273+
274+
if (workspace == null || !workspace.exists()) {
275+
LOGGER.fine("Workspace not available for job: " + job.getFullName());
276+
return null;
277+
}
278+
279+
// Check if .git directory exists
280+
FilePath gitDir = workspace.child(".git");
281+
if (!gitDir.exists()) {
282+
LOGGER.fine("No .git directory in workspace: " + workspace.getRemote());
283+
return null;
284+
}
285+
286+
LOGGER.info("Using existing workspace for branch fetch: " + workspace.getRemote());
287+
return fetchBranchesFromWorkspace(workspace);
288+
289+
} catch (Exception e) {
290+
LOGGER.log(Level.FINE, "Failed to fetch from workspace, will fall back to clone", e);
291+
return null;
292+
}
293+
}
294+
295+
/**
296+
* Fetch branches from an existing workspace by reading local refs only.
297+
* No network operation - instant response.
298+
* Returns null if no local refs found (caller should fall back to other methods).
299+
*/
300+
private List<BranchInfo> fetchBranchesFromWorkspace(FilePath workspace) throws IOException, InterruptedException {
301+
File workspaceDir = new File(workspace.getRemote());
302+
303+
TaskListener listener = new LogTaskListener(LOGGER, Level.INFO);
304+
EnvVars env = new EnvVars();
305+
306+
GitClient git = Git.with(listener, env)
307+
.in(workspaceDir)
308+
.using("jgit")
309+
.getClient();
310+
311+
// Only read existing local refs (instant, no network)
312+
List<BranchInfo> localRefs = readLocalRefs(git);
313+
if (!localRefs.isEmpty()) {
314+
LOGGER.info("Using cached local refs (" + localRefs.size() + " branches)");
315+
return applyLimits(localRefs);
316+
}
158317

159-
// Create a temp directory for the clone
318+
// No local refs found, return null to fall back to other methods
319+
LOGGER.info("No local refs found in workspace, will use fallback method");
320+
return null;
321+
}
322+
323+
/**
324+
* Read local refs from the repository without any network operation.
325+
* Returns empty list if no refs found or repository is invalid.
326+
*/
327+
private List<BranchInfo> readLocalRefs(GitClient git) {
328+
final List<BranchInfo> branchInfos = new ArrayList<>();
329+
330+
try {
331+
git.withRepository(new RepositoryCallback<Void>() {
332+
@Override
333+
public Void invoke(org.eclipse.jgit.lib.Repository repo, hudson.remoting.VirtualChannel channel) throws IOException {
334+
try (org.eclipse.jgit.revwalk.RevWalk walk = new org.eclipse.jgit.revwalk.RevWalk(repo)) {
335+
List<org.eclipse.jgit.lib.Ref> allRefs = repo.getRefDatabase().getRefsByPrefix("refs/remotes/origin/");
336+
for (org.eclipse.jgit.lib.Ref ref : allRefs) {
337+
String branchName = ref.getName().substring("refs/remotes/origin/".length());
338+
339+
if (branchName.equals("HEAD")) continue;
340+
341+
boolean isAlwaysIncluded = matchesAlwaysInclude(branchName);
342+
if (!isAlwaysIncluded && !matchesBranchFilter(branchName)) {
343+
continue;
344+
}
345+
346+
try {
347+
org.eclipse.jgit.revwalk.RevCommit commit = walk.parseCommit(ref.getObjectId());
348+
long commitTime = commit.getCommitTime() * 1000L;
349+
branchInfos.add(new BranchInfo(branchName, commitTime));
350+
} catch (Exception e) {
351+
branchInfos.add(new BranchInfo(branchName, 0L));
352+
}
353+
}
354+
}
355+
return null;
356+
}
357+
});
358+
} catch (Exception e) {
359+
LOGGER.log(Level.FINE, "Failed to read local refs", e);
360+
return new ArrayList<>();
361+
}
362+
363+
// Sort by commit date descending
364+
branchInfos.sort((a, b) -> Long.compare(b.getCommitTime(), a.getCommitTime()));
365+
return branchInfos;
366+
}
367+
368+
/**
369+
* Full clone to get commit timestamps (slowest but always works).
370+
*/
371+
private List<BranchInfo> fetchBranchesWithFullClone() throws IOException, InterruptedException {
372+
final List<BranchInfo> branchInfos = new ArrayList<>();
373+
374+
StandardCredentials credentials = getCredentials();
160375
File tempDir = createTempDirectory();
161376

162377
try {
@@ -196,7 +411,6 @@ public Void invoke(org.eclipse.jgit.lib.Repository repo, hudson.remoting.Virtual
196411
if (branchName.equals("HEAD")) continue;
197412

198413
// Apply branch filter
199-
// If alwaysIncludeBranches is set and matches, we keep it regardless of branchFilter
200414
boolean isAlwaysIncluded = matchesAlwaysInclude(branchName);
201415

202416
if (!isAlwaysIncluded && !matchesBranchFilter(branchName)) {
@@ -224,74 +438,44 @@ public Void invoke(org.eclipse.jgit.lib.Repository repo, hudson.remoting.Virtual
224438

225439
// Sort by commit date descending
226440
branchInfos.sort((a, b) -> Long.compare(b.getCommitTime(), a.getCommitTime()));
441+
442+
return applyLimits(branchInfos);
443+
}
227444

228-
// Limit to maxBranchCount, but keep all always-included branches
229-
if (branchInfos.size() > maxBranchCount) {
230-
if (alwaysIncludeBranches == null || alwaysIncludeBranches.trim().isEmpty()) {
231-
return branchInfos.subList(0, maxBranchCount);
232-
}
233-
234-
List<BranchInfo> limitedList = new ArrayList<>();
235-
int count = 0;
236-
237-
// First pass: add all branches that fit in the limit
238-
for (BranchInfo info : branchInfos) {
239-
boolean isAlwaysIncluded = matchesAlwaysInclude(info.getName());
240-
241-
if (count < maxBranchCount || isAlwaysIncluded) {
242-
limitedList.add(info);
243-
if (!isAlwaysIncluded) {
244-
count++; // Only count towards limit if NOT always included
245-
// Wait, if we don't count always included branches, we might end up with many branches.
246-
// Alternative strategy: Always included branches take priority, then fill up to maxBranchCount with others.
247-
} else {
248-
// If it IS always included, we add it. Does it consume a slot?
249-
// The requirement: "符合正则的分支不是最近活跃的分支也可以展示选项"
250-
// It implies they should be added EVEN IF they would fall out of the Top N.
251-
// So they are exceptions to the limit.
252-
}
253-
}
254-
}
255-
256-
// Let's refine the logic:
257-
// 1. Identify branches that MUST be included.
258-
// 2. Identify other branches that are candidates.
259-
// 3. Take all mandatory branches.
260-
// 4. Fill the remaining slots (up to maxBranchCount) with the best candidates.
261-
// 5. If mandatory branches already exceed maxBranchCount, we keep them all (and maybe no others).
262-
263-
// Wait, "maxBranchCount" usually implies total display count.
264-
// If I have 5 important branches and max=10, I show 5 important + 5 recent.
265-
// If I have 15 important branches and max=10, I show 15 important? Or 10 important?
266-
// Usually "Always Include" implies "Don't drop this". So I show 15.
267-
268-
List<BranchInfo> mandatory = new ArrayList<>();
269-
List<BranchInfo> others = new ArrayList<>();
270-
271-
for (BranchInfo info : branchInfos) {
272-
if (matchesAlwaysInclude(info.getName())) {
273-
mandatory.add(info);
274-
} else {
275-
others.add(info);
276-
}
445+
/**
446+
* Apply maxBranchCount limit while respecting alwaysIncludeBranches.
447+
*/
448+
private List<BranchInfo> applyLimits(List<BranchInfo> branchInfos) {
449+
if (branchInfos.size() <= maxBranchCount) {
450+
return branchInfos;
451+
}
452+
453+
if (alwaysIncludeBranches == null || alwaysIncludeBranches.trim().isEmpty()) {
454+
return new ArrayList<>(branchInfos.subList(0, maxBranchCount));
455+
}
456+
457+
List<BranchInfo> mandatory = new ArrayList<>();
458+
List<BranchInfo> others = new ArrayList<>();
459+
460+
for (BranchInfo info : branchInfos) {
461+
if (matchesAlwaysInclude(info.getName())) {
462+
mandatory.add(info);
463+
} else {
464+
others.add(info);
277465
}
278-
279-
List<BranchInfo> result = new ArrayList<>(mandatory);
280-
281-
// Fill remaining slots with others
282-
int slotsLeft = maxBranchCount - mandatory.size();
283-
if (slotsLeft > 0) {
284-
for (int i = 0; i < slotsLeft && i < others.size(); i++) {
285-
result.add(others.get(i));
286-
}
466+
}
467+
468+
List<BranchInfo> result = new ArrayList<>(mandatory);
469+
470+
// Fill remaining slots with others
471+
int slotsLeft = maxBranchCount - mandatory.size();
472+
if (slotsLeft > 0) {
473+
for (int i = 0; i < slotsLeft && i < others.size(); i++) {
474+
result.add(others.get(i));
287475
}
288-
289-
// Re-sort by time
290-
result.sort((a, b) -> Long.compare(b.getCommitTime(), a.getCommitTime()));
291-
return result;
292476
}
293477

294-
return branchInfos;
478+
return result;
295479
}
296480

297481
private boolean matchesAlwaysInclude(String branchName) {

0 commit comments

Comments
 (0)