@@ -123,6 +123,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
123123
124124 // 7. Assemble the run request.
125125 let ( branch, commit) = git_metadata ( & repo_root, args. branch . clone ( ) ) ;
126+ let repo_name = git_remote_repo_name ( & repo_root) ;
126127 let req = hm_exec:: RunRequest {
127128 plan,
128129 repo_root,
@@ -132,6 +133,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
132133 branch,
133134 commit,
134135 message : args. message . clone ( ) ,
136+ repo_name,
135137 } ,
136138 options : hm_exec:: RunOptions {
137139 no_cache : false ,
@@ -202,6 +204,43 @@ fn git_metadata(root: &std::path::Path, branch_override: Option<String>) -> (Str
202204 ( branch, commit)
203205}
204206
207+ /// Parse `owner/repo` from a git remote URL, mirroring the backend's
208+ /// `Harmont.Pipelines.RepoName`: drop scheme/host and a trailing `.git`, then
209+ /// take the last two non-empty path segments. `None` when fewer than two
210+ /// segments remain.
211+ fn parse_repo_name ( url : & str ) -> Option < String > {
212+ let url = url. trim ( ) ;
213+ let path = if let Some ( ( _, rest) ) = url. split_once ( "://" ) {
214+ // scheme://host/owner/repo → strip host
215+ rest. split_once ( '/' ) . map_or ( rest, |( _, p) | p)
216+ } else if url. contains ( '@' ) && url. contains ( ':' ) {
217+ // scp-style git@host:owner/repo → strip "git@host:"
218+ let after_at = url. split_once ( '@' ) . map_or ( url, |( _, r) | r) ;
219+ after_at. split_once ( ':' ) . map_or ( after_at, |( _, p) | p)
220+ } else {
221+ url. split_once ( '/' ) . map_or ( url, |( _, p) | p)
222+ } ;
223+ let path = path. trim_end_matches ( '/' ) ;
224+ let path = path. strip_suffix ( ".git" ) . unwrap_or ( path) ;
225+ let segs: Vec < & str > = path. split ( '/' ) . filter ( |s| !s. is_empty ( ) ) . collect ( ) ;
226+ if segs. len ( ) < 2 {
227+ return None ;
228+ }
229+ Some ( segs[ segs. len ( ) - 2 ..] . join ( "/" ) )
230+ }
231+
232+ /// Best-effort `owner/repo` from the worktree's `origin` remote.
233+ fn git_remote_repo_name ( root : & std:: path:: Path ) -> Option < String > {
234+ let out = std:: process:: Command :: new ( "git" )
235+ . arg ( "-C" )
236+ . arg ( root)
237+ . args ( [ "config" , "--get" , "remote.origin.url" ] )
238+ . output ( )
239+ . ok ( )
240+ . filter ( |o| o. status . success ( ) ) ?;
241+ parse_repo_name ( & String :: from_utf8_lossy ( & out. stdout ) )
242+ }
243+
205244/// Resolve repo root, detect the DSL, select the pipeline slug, and render
206245/// the v0 IR JSON. Shared by local and cloud runs.
207246///
@@ -375,6 +414,32 @@ error[backend]: {other}
375414mod tests {
376415 use super :: * ;
377416
417+ #[ test]
418+ fn parse_repo_name_handles_https_ssh_and_scp ( ) {
419+ assert_eq ! (
420+ parse_repo_name( "https://github.com/harmont-dev/harmont-cli.git" ) . as_deref( ) ,
421+ Some ( "harmont-dev/harmont-cli" )
422+ ) ;
423+ assert_eq ! (
424+ parse_repo_name( "git@github.com:harmont-dev/harmont-cli.git" ) . as_deref( ) ,
425+ Some ( "harmont-dev/harmont-cli" )
426+ ) ;
427+ assert_eq ! (
428+ parse_repo_name( "ssh://git@github.com/harmont-dev/harmont-cli" ) . as_deref( ) ,
429+ Some ( "harmont-dev/harmont-cli" )
430+ ) ;
431+ assert_eq ! (
432+ parse_repo_name( "https://example.com/a/b/c/repo" ) . as_deref( ) ,
433+ Some ( "c/repo" )
434+ ) ;
435+ }
436+
437+ #[ test]
438+ fn parse_repo_name_rejects_unparseable ( ) {
439+ assert_eq ! ( parse_repo_name( "" ) , None ) ;
440+ assert_eq ! ( parse_repo_name( "not-a-url" ) , None ) ;
441+ }
442+
378443 #[ test]
379444 fn parse_env_splits_pairs ( ) {
380445 let m = parse_env ( & [ "A=1" . into ( ) , "B=x=y" . into ( ) , "bad" . into ( ) ] ) ;
0 commit comments