@@ -83,16 +83,79 @@ def _get_username(data):
8383    return  "" 
8484
8585
86+ async  def  _validate_time_from_last_commit_to_pr_update (data : dict ) ->  bool :
87+     is_valid_push  =  False 
88+     try :
89+         data_inner  =  data .get ('data' , {})
90+         if  not  data_inner :
91+             get_logger ().error ("No data found in the webhook payload" )
92+             return  True 
93+         pull_request  =  data_inner .get ('pullrequest' , {})
94+         commits_api  =  pull_request .get ('links' , {}).get ('commits' , {}).get ('href' )
95+         if  not  commits_api :
96+             return  False 
97+         if  not  pull_request .get ('updated_on' ):
98+             return  False 
99+         bearer_token  =  context .get ('bitbucket_bearer_token' )
100+         headers  =  {
101+             'Authorization' : f'Bearer { bearer_token }  ,
102+             'Accept' : 'application/json' 
103+         }
104+         response  =  requests .get (commits_api , headers = headers )
105+         if  response .status_code  !=  200 :
106+             get_logger ().warning (f"Bitbucket commits API returned { response .status_code } { commits_api }  )
107+             return  False 
108+ 
109+         username  = _get_username (data )
110+         commits_data  =  response .json () or  {}
111+         values  =  commits_data .get ('values' ) or  []
112+         if  (not  values  or  not  isinstance (values , list ) or  not  values [0 ].get ('author' ) or  not  values [0 ]['author' ].get ('user' )
113+                 or  not  values [0 ]['author' ]['user' ].get ('display_name' )):
114+             get_logger ().warning ("No commits returned for pull request or one of the required fields missing; skipping push validation" ,
115+                                  artifact = {'values' : values })
116+             return  False 
117+         commit_username  =  commits_data ['values' ][0 ]['author' ]['user' ]['display_name' ]
118+         if  username  !=  commit_username :
119+             get_logger ().warning (f"Mismatch in username { username } { commit_username }  )
120+             return  False 
121+ 
122+         time_pr_updated  =  pull_request ['updated_on' ]
123+         time_last_commit  =  commits_data ['values' ][0 ]['date' ]
124+         from  datetime  import  datetime 
125+         ts1  =  datetime .fromisoformat (time_pr_updated )
126+         ts2  =  datetime .fromisoformat (time_last_commit )
127+         diff  =  (ts1  -  ts2 ).total_seconds ()
128+         max_delta_seconds  =  15 
129+         if  diff  >  0  and  diff  <  max_delta_seconds :
130+             is_valid_push  =  True 
131+         else :
132+             get_logger ().debug (f"Too much time passed since last commit" ,
133+                                artifact = {'updated' : time_pr_updated , 'last_commit' : time_last_commit })
134+     except  Exception  as  e :
135+         get_logger ().exception (f"Failed to validate time difference between last commit and PR update" ,
136+                                artifact = {'error' : e , 'data' : data })
137+     return  is_valid_push 
138+ 
86139async  def  _perform_commands_bitbucket (commands_conf : str , agent : PRAgent , api_url : str , log_context : dict , data : dict ):
87140    apply_repo_settings (api_url )
88141    if  commands_conf  ==  "pr_commands"  and  get_settings ().config .disable_auto_feedback :  # auto commands for PR, and auto feedback is disabled 
89142        get_logger ().info (f"Auto feedback is disabled, skipping auto commands for PR { api_url = }  )
90143        return 
144+     if  commands_conf  ==  "push_commands" :
145+         if  not  get_settings ().get ("bitbucket_app.handle_push_trigger" ):
146+             get_logger ().info (
147+                 "Bitbucket push trigger handling disabled via config; skipping push commands" )
148+             return 
91149    if  data .get ("event" , "" ) ==  "pullrequest:created" :
92150        if  not  should_process_pr_logic (data ):
93151            return 
94152    commands  =  get_settings ().get (f"bitbucket_app.{ commands_conf }  , {})
95153    get_settings ().set ("config.is_auto_command" , True )
154+     if  commands_conf  ==  "push_commands" :
155+         is_valid_push  =  await  _validate_time_from_last_commit_to_pr_update (data )
156+         if  not  is_valid_push :
157+             get_logger ().info (f"Bitbucket skipping 'pullrequest:updated' for push commands" )
158+             return 
96159    for  command  in  commands :
97160        try :
98161            split_command  =  command .split (" " )
@@ -215,11 +278,21 @@ async def inner():
215278                log_context ["event" ] =  "pull_request" 
216279                if  pr_url :
217280                    with  get_logger ().contextualize (** log_context ):
218-                         apply_repo_settings (pr_url )
219281                        if  get_identity_provider ().verify_eligibility ("bitbucket" ,
220282                                                        sender_id , pr_url ) is  not Eligibility .NOT_ELIGIBLE :
221283                            if  get_settings ().get ("bitbucket_app.pr_commands" ):
222-                                 await  _perform_commands_bitbucket ("pr_commands" , PRAgent (), pr_url , log_context , data )
284+                                 await  _perform_commands_bitbucket ("pr_commands" , agent , pr_url , log_context , data )
285+             elif  event  ==  "pullrequest:updated" : # PR updated, might be from a push (we will validate this later) 
286+                 pr_url  =  data ["data" ]["pullrequest" ]["links" ]["html" ]["href" ]
287+                 log_context ["api_url" ] =  pr_url 
288+                 log_context ["event" ] =  "pull_request" 
289+                 if  pr_url :
290+                     with  get_logger ().contextualize (** log_context ):
291+                         if  get_identity_provider ().verify_eligibility ("bitbucket" ,
292+                                                         sender_id , pr_url ) is  not Eligibility .NOT_ELIGIBLE :
293+ 
294+                             if  get_settings ().get ("bitbucket_app.push_commands" ):
295+                                 await  _perform_commands_bitbucket ("push_commands" , agent , pr_url , log_context , data )
223296            elif  event  ==  "pullrequest:comment_created" :
224297                pr_url  =  data ["data" ]["pullrequest" ]["links" ]["html" ]["href" ]
225298                log_context ["api_url" ] =  pr_url 
0 commit comments