@@ -298,7 +298,7 @@ pub enum RevsetExpression<St: ExpressionState> {
298298    Bisect ( Arc < Self > ) , 
299299    HasSize  { 
300300        candidates :  Arc < Self > , 
301-         count :   usize , 
301+         range :   Range < usize > , 
302302    } , 
303303    Latest  { 
304304        candidates :  Arc < Self > , 
@@ -533,10 +533,10 @@ impl<St: ExpressionState> RevsetExpression<St> {
533533    } 
534534
535535    /// Commits in `self`, the number of which must be exactly equal to `count`. 
536- pub  fn  has_size ( self :  & Arc < Self > ,  count :   usize )  -> Arc < Self >  { 
536+ pub  fn  has_size ( self :  & Arc < Self > ,  range :   Range < usize > )  -> Arc < Self >  { 
537537        Arc :: new ( Self :: HasSize  { 
538538            candidates :  self . clone ( ) , 
539-             count , 
539+             range , 
540540        } ) 
541541    } 
542542
@@ -736,7 +736,7 @@ pub enum ResolvedExpression {
736736    Bisect ( Box < Self > ) , 
737737    HasSize  { 
738738        candidates :  Box < Self > , 
739-         count :   usize , 
739+         range :   Range < usize > , 
740740    } , 
741741    Latest  { 
742742        candidates :  Box < Self > , 
@@ -948,10 +948,13 @@ static BUILTIN_FUNCTION_MAP: LazyLock<HashMap<&str, RevsetFunction>> = LazyLock:
948948        Ok ( RevsetExpression :: bisect ( & expression) ) 
949949    } ) ; 
950950    map. insert ( "exactly" ,  |diagnostics,  function,  context| { 
951-         let  ( [ candidates_arg,  count_arg ] ,  [ ] )  = function. expect_arguments ( ) ?; 
951+         let  ( [ candidates_arg,  range_arg ] ,  [ ] )  = function. expect_arguments ( ) ?; 
952952        let  candidates = lower_expression ( diagnostics,  candidates_arg,  context) ?; 
953-         let  count = expect_literal ( "integer" ,  count_arg) ?; 
954-         Ok ( candidates. has_size ( count) ) 
953+         let  range_str:  String  = expect_literal ( "string" ,  range_arg) ?; 
954+         let  range = parse_range ( & range_str) 
955+             . map_err ( |msg| RevsetParseError :: expression ( msg,  range_arg. span ) ) ?; 
956+ 
957+         Ok ( candidates. has_size ( range) ) 
955958    } ) ; 
956959    map. insert ( "merges" ,  |_diagnostics,  function,  _context| { 
957960        function. expect_no_arguments ( ) ?; 
@@ -1161,6 +1164,44 @@ pub fn expect_date_pattern(
11611164    } ) 
11621165} 
11631166
1167+ fn  parse_range ( s :  & str )  -> Result < Range < usize > ,  String >  { 
1168+     if  let  Ok ( n)  = s. parse :: < usize > ( )  { 
1169+         return  Ok ( n..n + 1 ) ; 
1170+     } 
1171+ 
1172+     // inclusive range syntax (a..=b) 
1173+     if  let  Some ( ( start_str,  end_str) )  = s. split_once ( "..=" )  { 
1174+         let  start = start_str
1175+             . parse :: < usize > ( ) 
1176+             . map_err ( |_| format ! ( "Invalid start of range: {start_str}" ) ) ?; 
1177+         let  end = end_str
1178+             . parse :: < usize > ( ) 
1179+             . map_err ( |_| format ! ( "Invalid end of range: {end_str}" ) ) ?; 
1180+         if  start > end { 
1181+             return  Err ( format ! ( "Invalid range: start ({start}) > end ({end})" ) ) ; 
1182+         } 
1183+         return  Ok ( start..end + 1 ) ; 
1184+     } 
1185+ 
1186+     // exclusive range syntax (a..b) 
1187+     if  let  Some ( ( start_str,  end_str) )  = s. split_once ( ".." )  { 
1188+         let  start = start_str
1189+             . parse :: < usize > ( ) 
1190+             . map_err ( |_| format ! ( "Invalid start of range: {start_str}" ) ) ?; 
1191+         let  end = end_str
1192+             . parse :: < usize > ( ) 
1193+             . map_err ( |_| format ! ( "Invalid end of range: {end_str}" ) ) ?; 
1194+         if  start > end { 
1195+             return  Err ( format ! ( "Invalid range: start ({start}) > end ({end})" ) ) ; 
1196+         } 
1197+         return  Ok ( start..end) ; 
1198+     } 
1199+ 
1200+     Err ( format ! ( 
1201+         "Invalid range syntax: '{s}'. Expected format: 'n', 'n..m', or 'n..=m'" 
1202+     ) ) 
1203+ } 
1204+ 
11641205fn  parse_remote_bookmarks_arguments ( 
11651206    diagnostics :  & mut  RevsetDiagnostics , 
11661207    function :  & FunctionCallNode , 
@@ -1472,10 +1513,10 @@ fn try_transform_expression<St: ExpressionState, E>(
14721513            RevsetExpression :: Bisect ( expression)  => { 
14731514                transform_rec ( expression,  pre,  post) ?. map ( RevsetExpression :: Bisect ) 
14741515            } 
1475-             RevsetExpression :: HasSize  {  candidates,  count  }  => { 
1516+             RevsetExpression :: HasSize  {  candidates,  range  }  => { 
14761517                transform_rec ( candidates,  pre,  post) ?. map ( |candidates| RevsetExpression :: HasSize  { 
14771518                    candidates, 
1478-                     count :   * count , 
1519+                     range :  range . clone ( ) , 
14791520                } ) 
14801521            } 
14811522            RevsetExpression :: Latest  {  candidates,  count }  => transform_rec ( candidates,  pre,  post) ?
@@ -1719,11 +1760,11 @@ where
17191760            let  expression = folder. fold_expression ( expression) ?; 
17201761            RevsetExpression :: Bisect ( expression) . into ( ) 
17211762        } 
1722-         RevsetExpression :: HasSize  {  candidates,  count  }  => { 
1763+         RevsetExpression :: HasSize  {  candidates,  range  }  => { 
17231764            let  candidates = folder. fold_expression ( candidates) ?; 
17241765            RevsetExpression :: HasSize  { 
17251766                candidates, 
1726-                 count :   * count , 
1767+                 range :  range . clone ( ) , 
17271768            } 
17281769            . into ( ) 
17291770        } 
@@ -3047,9 +3088,9 @@ impl VisibilityResolutionContext<'_> {
30473088                candidates :  self . resolve ( candidates) . into ( ) , 
30483089                count :  * count, 
30493090            } , 
3050-             RevsetExpression :: HasSize  {  candidates,  count  }  => ResolvedExpression :: HasSize  { 
3091+             RevsetExpression :: HasSize  {  candidates,  range  }  => ResolvedExpression :: HasSize  { 
30513092                candidates :  self . resolve ( candidates) . into ( ) , 
3052-                 count :   * count , 
3093+                 range :  range . clone ( ) , 
30533094            } , 
30543095            RevsetExpression :: Filter ( _)  | RevsetExpression :: AsFilter ( _)  => { 
30553096                // Top-level filter without intersection: e.g. "~author(_)" is represented as 
0 commit comments