77import com .cloudbees .plugins .credentials .domains .URIRequirementBuilder ;
88import edu .umd .cs .findbugs .annotations .NonNull ;
99import hudson .Extension ;
10+ import hudson .FilePath ;
11+ import hudson .model .AbstractProject ;
1012import hudson .model .Item ;
13+ import hudson .model .Job ;
1114import hudson .model .ParameterDefinition ;
1215import hudson .model .ParameterValue ;
1316import hudson .security .ACL ;
2629import org .kohsuke .stapler .DataBoundConstructor ;
2730import org .kohsuke .stapler .DataBoundSetter ;
2831import org .kohsuke .stapler .QueryParameter ;
32+ import org .kohsuke .stapler .Stapler ;
2933import org .kohsuke .stapler .StaplerRequest ;
3034
3135import java .io .File ;
3236import java .io .IOException ;
3337import java .util .*;
38+ import java .util .Collections ;
3439import java .util .logging .Level ;
3540import java .util .logging .Logger ;
3641import 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