def merge(self): """Tries to merge the new content from the remote repository. If there is a merge conflict, sets state to ALERT_CONFLICT, otherwise sets state to EXPORT. """ result, stdout, _ = utils.capture_command('git', 'merge', '--squash', self.__last_synced, expect_success=False) print stdout if result == 0: sync_commit_message = 'Syncing review %s at %s.' % ( self.__branch, self.__last_synced) # TODO(dhermes): Catch error here. print utils.capture_command('git', 'commit', '-m', sync_commit_message, single_line=False) self.state = self.EXPORT else: self.state = self.ALERT_CONFLICT self.advance()
def check_environment(self): """Checks that a sync can be performed. If a sync can't be performed, sets state to FINISHED. If it can be, sets state to CHECK_CONTINUE or CHECK_NEW, depending on whether the sync is a continue sync or a new sync. """ # Make sure branch is clean if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (self.__branch,) print utils.capture_command('git', 'diff', single_line=False) self.state = self.FINISHED else: # TODO(dhermes): This assumes review_info is not None. Fix this. self.__last_commit = self.__rietveld_info.review_info.last_commit # Using getattr since SYNC_HALTED is not an explicit attribute in # RietveldInfo, hence accessing rietveld_info.sync_halted may result # in an AttributeError. self.__sync_halted = getattr(self.__rietveld_info, utils.SYNC_HALTED, False) if self.__continue: self.state = self.CHECK_CONTINUE else: self.state = self.CHECK_NEW self.advance()
def check_environment(self): """Checks that a sync can be performed. If a sync can't be performed, sets state to FINISHED. If it can be, sets state to CHECK_CONTINUE or CHECK_NEW, depending on whether the sync is a continue sync or a new sync. """ # Make sure branch is clean if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (self.__branch, ) print utils.capture_command('git', 'diff', single_line=False) self.state = self.FINISHED else: # TODO(dhermes): This assumes review_info is not None. Fix this. self.__last_commit = self.__rietveld_info.review_info.last_commit # Using getattr since SYNC_HALTED is not an explicit attribute in # RietveldInfo, hence accessing rietveld_info.sync_halted may result # in an AttributeError. self.__sync_halted = getattr(self.__rietveld_info, utils.SYNC_HALTED, False) if self.__continue: self.state = self.CHECK_CONTINUE else: self.state = self.CHECK_NEW self.advance()
def rename(self, rietveld_info): """Renames the source branch and moves the Rietveld info as well. If successful, sets state to FINISHED. Args: rietveld_info: RietveldInfo object for the branch. Is copied over to the new branch. Raises: GitRvException: If rietveld_info is None. """ if rietveld_info is None: raise utils.GitRvException('Rename command received unexpected ' 'branch info.') print 'Renaming branch...' print utils.capture_command('git', 'branch', '-m', self.__source_branch, self.__target_branch, single_line=False) print 'Moving review info.' rietveld_info._branch_name = self.__target_branch rietveld_info.save() utils.RietveldInfo.remove(branch_name=self.__source_branch)
def clean_up_local(self, success=False): """Cleans up the repository after a commit or failure. Deletes the dummy branch created and checks back out the review branch. If the SubmitAction was successful (success), then the branch metadata is removed from the git config. In addition, the review branch will be replaced with HEAD at the newly submitted commit. If the SubmitAction was not successful, all other cleanup for the failure case is considered to have been done before. If the SubmitAction was successful, sets state to CLEAN_UP_REVIEW, otherwise sets it to FINISHED. In either case, advances the state machine. Args: success: Boolean indicating whether or not the submit succeeded. """ if success: print ('Replacing review branch %r with newly ' 'committed content.' % (self.__branch,)) # Remove the review branch utils.capture_command('git', 'branch', '-D', self.__branch, single_line=False) # TODO(dhermes): The git push will update the locally stored # version of the remote. Is this enough to guarantee # we are doing the right thing here? # Add back the review branch with HEAD at the new commit utils.capture_command( 'git', 'branch', '--track', self.__branch, self.__rietveld_info.remote_info.remote_branch_ref, single_line=False) # Remove Rietveld metadata associated with the review branch utils.RietveldInfo.remove(branch_name=self.__branch) # Check out the review branch. We use -f in case we failed in a detached # HEAD or dirty state and want to get back to our clean branch. utils.capture_command('git', 'checkout', '-f', self.__branch, single_line=False) # This brings the review branch back to a stable state, which it was # required to be in by check_environment(). If there are no pending # changes left over from the checkout -f, this hard reset does nothing. utils.capture_command('git', 'reset', '--hard', 'HEAD', single_line=False) # If __review_branch was set, we know we have a dummy branch created # by this action which must be deleted. if self.__review_branch is not None: utils.capture_command('git', 'branch', '-D', self.__review_branch, single_line=False) if success: self.state = self.CLEAN_UP_REVIEW else: self.state = self.FINISHED self.advance()
def delete(self): """Deletes the branch and the Rietveld info as well. If successful, sets state to FINISHED. """ print 'Deleting branch...' print utils.capture_command('git', 'branch', '-D', self.__branch, single_line=False) print 'Deleting review info.' # TODO(dhermes): Consider closing this issue as well, or adding a flag # to do so. utils.RietveldInfo.remove(branch_name=self.__branch)
def callback(cls, args, argv): """A callback to begin an ExportAction after arguments are parsed. If the branch is not in a clean state, won't create an ExportAction, will just print 'git diff' and proceed. Args: args: An argparse.Namespace object to extract parameters from. argv: The original command line arguments that were parsed to create args. These may be used in a call to upload.py. Returns: An instance of ExportAction. Just by instantiating the instance, the state machine will begin working. """ current_branch = utils.get_current_branch() if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (current_branch, ) print utils.capture_command('git', 'diff', single_line=False) return if args.no_mail and args.send_patch: raise GitRvException('The flags --no_mail and --send_patch are ' 'mutually exclusive.') # This is to determine whether or not --send_mail should be added to # the upload.py call. If --send_patch is set, we don't need to # send mail. Similarly if --no_mail is set, we should not send mail. no_send_mail = args.no_mail or args.send_patch # Rietveld treats an empty string the same as if the value # was never set. if args.message and not args.title: raise GitRvException('A patch description can only be set if it ' 'also has a title.') commit_subject = commit_description = None if args.title: if len(args.title) > 100: raise GitRvException(utils.SUBJECT_TOO_LONG_TEMPLATE % (args.title, )) # If args.message is '', then both the Title and Description # will take the value of the title. commit_subject = args.title commit_description = args.message or '' return cls(current_branch, args, commit_subject=commit_subject, commit_description=commit_description, no_send_mail=no_send_mail, argv=argv)
def callback(cls, args, argv): """A callback to begin an ExportAction after arguments are parsed. If the branch is not in a clean state, won't create an ExportAction, will just print 'git diff' and proceed. Args: args: An argparse.Namespace object to extract parameters from. argv: The original command line arguments that were parsed to create args. These may be used in a call to upload.py. Returns: An instance of ExportAction. Just by instantiating the instance, the state machine will begin working. """ current_branch = utils.get_current_branch() if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (current_branch,) print utils.capture_command('git', 'diff', single_line=False) return if args.no_mail and args.send_patch: raise GitRvException('The flags --no_mail and --send_patch are ' 'mutually exclusive.') # This is to determine whether or not --send_mail should be added to # the upload.py call. If --send_patch is set, we don't need to # send mail. Similarly if --no_mail is set, we should not send mail. no_send_mail = args.no_mail or args.send_patch # Rietveld treats an empty string the same as if the value # was never set. if args.message and not args.title: raise GitRvException('A patch description can only be set if it ' 'also has a title.') commit_subject = commit_description = None if args.title: if len(args.title) > 100: raise GitRvException(utils.SUBJECT_TOO_LONG_TEMPLATE % (args.title,)) # If args.message is '', then both the Title and Description # will take the value of the title. commit_subject = args.title commit_description = args.message or '' return cls(current_branch, args, commit_subject=commit_subject, commit_description=commit_description, no_send_mail=no_send_mail, argv=argv)
def set_history_from_remote(self): """Sets history in detached HEAD to the remote history. Uses a soft reset to add the commit history from the last synced commit in the remote branch. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to CREATE_BRANCH; if not, saves the error message and sets state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Soft reset to add remote branch commit history print 'Setting head at %s.' % (self.__last_synced, ) result, _, stderr = utils.capture_command('git', 'reset', '--soft', self.__last_synced, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.CREATE_BRANCH self.advance(**next_state_kwargs)
def check_environment(self): """Checks that the current review branch is in a clean state. If not, we can't submit, so sets state to FINISHED after notifying the user of the issue. If it can be, sets state to VERIFY_APPROVAL. In either case, advances the state machine. """ # Make sure branch is clean if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (self.__branch, ) print utils.capture_command('git', 'diff', single_line=False) self.state = self.FINISHED else: self.state = self.VERIFY_APPROVAL self.advance()
def push_commit(self): """Pushes the squashed commit to the remote repository. If the push fails, saves the error message so it can be used to notify the user. If successful, sets state to CLEAN_UP_LOCAL, otherwise to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} branch_mapping = '%s:%s' % (self.__review_branch, self.__remote_branch) result, _, stderr = utils.capture_command('git', 'push', self.__remote, branch_mapping, expect_success=False) if result != 0: # TODO(dhermes): Should we try a sync and proceed if no failure? next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: next_state_kwargs['success'] = True self.state = self.CLEAN_UP_LOCAL self.advance(**next_state_kwargs)
def check_environment(self): """Checks that the current review branch is in a clean state. If not, we can't submit, so sets state to FINISHED after notifying the user of the issue. If it can be, sets state to VERIFY_APPROVAL. In either case, advances the state machine. """ # Make sure branch is clean if not utils.in_clean_state(): print 'Branch %r not in clean state:' % (self.__branch,) print utils.capture_command('git', 'diff', single_line=False) self.state = self.FINISHED else: self.state = self.VERIFY_APPROVAL self.advance()
def set_history_from_remote(self): """Sets history in detached HEAD to the remote history. Uses a soft reset to add the commit history from the last synced commit in the remote branch. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to CREATE_BRANCH; if not, saves the error message and sets state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Soft reset to add remote branch commit history print 'Setting head at %s.' % (self.__last_synced,) result, _, stderr = utils.capture_command( 'git', 'reset', '--soft', self.__last_synced, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.CREATE_BRANCH self.advance(**next_state_kwargs)
def enter_detached_state(self): """Enters detached HEAD state with review contents. Enters a detached HEAD state holding the contents of the review branch, but none of the history. This is so we can rewrite the history to apply the reviewed work to the existing history of the remote branch. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to SET_HISTORY_FROM_REMOTE; if not, saves the error message and sets state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Enter detached HEAD state print 'Entering detached HEAD state with contents from %s.' % ( self.__branch,) current_branch_detached = '%s@{0}' % (self.__branch,) result, _, stderr = utils.capture_command( 'git', 'checkout', current_branch_detached, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.SET_HISTORY_FROM_REMOTE self.advance(**next_state_kwargs)
def push_commit(self): """Pushes the squashed commit to the remote repository. If the push fails, saves the error message so it can be used to notify the user. If successful, sets state to CLEAN_UP_LOCAL, otherwise to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} branch_mapping = '%s:%s' % (self.__review_branch, self.__remote_branch) result, _, stderr = utils.capture_command( 'git', 'push', self.__remote, branch_mapping, expect_success=False) if result != 0: # TODO(dhermes): Should we try a sync and proceed if no failure? next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: next_state_kwargs['success'] = True self.state = self.CLEAN_UP_LOCAL self.advance(**next_state_kwargs)
def enter_detached_state(self): """Enters detached HEAD state with review contents. Enters a detached HEAD state holding the contents of the review branch, but none of the history. This is so we can rewrite the history to apply the reviewed work to the existing history of the remote branch. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to SET_HISTORY_FROM_REMOTE; if not, saves the error message and sets state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Enter detached HEAD state print 'Entering detached HEAD state with contents from %s.' % ( self.__branch, ) current_branch_detached = '%s@{0}' % (self.__branch, ) result, _, stderr = utils.capture_command('git', 'checkout', current_branch_detached, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.SET_HISTORY_FROM_REMOTE self.advance(**next_state_kwargs)
def fetch_remote(self): """Fetchs the remote associated with the current review. If the fetched remote has no new commits, sets state to FINISHED, otherwise sets state to MERGE_REMOTE_IN. """ # TODO(dhermes): This assumes remote_info is not None. Fix this. remote_info = self.__rietveld_info.remote_info print utils.capture_command('git', 'fetch', remote_info.remote, single_line=False) new_head_in_remote = remote_info.head_in_remote_branch if new_head_in_remote == self.__rietveld_info.remote_info.last_synced: print 'No new changes in %s.' % (remote_info.remote_branch_ref,) self.state = self.FINISHED else: self.state = self.MERGE_REMOTE_IN self.__last_synced = new_head_in_remote self.advance()
def merge(self): """Tries to merge the new content from the remote repository. If there is a merge conflict, sets state to ALERT_CONFLICT, otherwise sets state to EXPORT. """ result, stdout, _ = utils.capture_command( 'git', 'merge', '--squash', self.__last_synced, expect_success=False) print stdout if result == 0: sync_commit_message = 'Syncing review %s at %s.' % ( self.__branch, self.__last_synced) # TODO(dhermes): Catch error here. print utils.capture_command('git', 'commit', '-m', sync_commit_message, single_line=False) self.state = self.EXPORT else: self.state = self.ALERT_CONFLICT self.advance()
def fetch_remote(self): """Fetchs the remote associated with the current review. If the fetched remote has no new commits, sets state to FINISHED, otherwise sets state to MERGE_REMOTE_IN. """ # TODO(dhermes): This assumes remote_info is not None. Fix this. remote_info = self.__rietveld_info.remote_info print utils.capture_command('git', 'fetch', remote_info.remote, single_line=False) new_head_in_remote = remote_info.head_in_remote_branch if new_head_in_remote == self.__rietveld_info.remote_info.last_synced: print 'No new changes in %s.' % (remote_info.remote_branch_ref, ) self.state = self.FINISHED else: self.state = self.MERGE_REMOTE_IN self.__last_synced = new_head_in_remote self.advance()
def create_branch(self): """Creates dummy branch with contents from detached HEAD. - Finds a dummy name by using BRANCH_NAME_TEMPLATE and the current issue and then adding '_0' until it finds a branch name which doesn't already exist. - Creates and checks out (via checkout -b) the contents using the dummy name. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to COMMIT; if not, saves the error message and state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Find dummy branch name review_branch = BRANCH_NAME_TEMPLATE % self.__issue while utils.branch_exists(review_branch): review_branch += '_0' # Dictionary to pass along state to advance() next_state_kwargs = {} # Create and checkout review branch print 'Checking out %s at %s.' % (review_branch, self.__last_synced) result, _, stderr = utils.capture_command('git', 'checkout', '-b', review_branch, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: # Only set the review branch if it is created. self.__review_branch = review_branch self.state = self.COMMIT self.advance(**next_state_kwargs)
def commit(self): """Adds reviewed changes to stable contents in dummy branch. Commits the current content as a single commit (extra) in this remote branch history (but in the local branch). Uses the issue description (from the review) and adds a note about the review. If successful, sets state to PUSHING; if not, saves the error message and state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Commit the current content description_newline = '' if self.__description: description_newline = '\n\n' final_commit_message = utils.SQUASH_COMMIT_TEMPLATE % { utils.SUBJECT: self.__subject, utils.DESCRIPTION_NEWLINE: description_newline, utils.ISSUE_DESCRIPTION: self.__description, utils.ISSUE: self.__issue, utils.SERVER: self.__server, } print 'Adding commit:' print final_commit_message result, _, stderr = utils.capture_command('git', 'commit', '-m', final_commit_message, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.PUSHING # Advance self.advance(**next_state_kwargs)
def create_branch(self): """Creates dummy branch with contents from detached HEAD. - Finds a dummy name by using BRANCH_NAME_TEMPLATE and the current issue and then adding '_0' until it finds a branch name which doesn't already exist. - Creates and checks out (via checkout -b) the contents using the dummy name. Thanks to http://stackoverflow.com/a/4481621/1068170 for the merge strategy. If successful, sets state to COMMIT; if not, saves the error message and state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Find dummy branch name review_branch = BRANCH_NAME_TEMPLATE % self.__issue while utils.branch_exists(review_branch): review_branch += '_0' # Dictionary to pass along state to advance() next_state_kwargs = {} # Create and checkout review branch print 'Checking out %s at %s.' % (review_branch, self.__last_synced) result, _, stderr = utils.capture_command( 'git', 'checkout', '-b', review_branch, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: # Only set the review branch if it is created. self.__review_branch = review_branch self.state = self.COMMIT self.advance(**next_state_kwargs)
def commit(self): """Adds reviewed changes to stable contents in dummy branch. Commits the current content as a single commit (extra) in this remote branch history (but in the local branch). Uses the issue description (from the review) and adds a note about the review. If successful, sets state to PUSHING; if not, saves the error message and state to NOTIFY_FAILURE. In either case, advances the state machine. """ # Dictionary to pass along state to advance() next_state_kwargs = {} # Commit the current content description_newline = '' if self.__description: description_newline = '\n\n' final_commit_message = utils.SQUASH_COMMIT_TEMPLATE % { utils.SUBJECT: self.__subject, utils.DESCRIPTION_NEWLINE: description_newline, utils.ISSUE_DESCRIPTION: self.__description, utils.ISSUE: self.__issue, utils.SERVER: self.__server, } print 'Adding commit:' print final_commit_message result, _, stderr = utils.capture_command( 'git', 'commit', '-m', final_commit_message, expect_success=False) if result != 0: next_state_kwargs['error_message'] = stderr self.state = self.NOTIFY_FAILURE else: self.state = self.PUSHING # Advance self.advance(**next_state_kwargs)
def clean_up_local(self, success=False): """Cleans up the repository after a commit or failure. Deletes the dummy branch created and checks back out the review branch. If the SubmitAction was successful (success), then the branch metadata is removed from the git config. In addition, the review branch will be replaced with HEAD at the newly submitted commit. If the SubmitAction was not successful, all other cleanup for the failure case is considered to have been done before. If the SubmitAction was successful, sets state to CLEAN_UP_REVIEW, otherwise sets it to FINISHED. In either case, advances the state machine. Args: success: Boolean indicating whether or not the submit succeeded. """ if success: print( 'Replacing review branch %r with newly ' 'committed content.' % (self.__branch, )) # Remove the review branch utils.capture_command('git', 'branch', '-D', self.__branch, single_line=False) # TODO(dhermes): The git push will update the locally stored # version of the remote. Is this enough to guarantee # we are doing the right thing here? # Add back the review branch with HEAD at the new commit utils.capture_command( 'git', 'branch', '--track', self.__branch, self.__rietveld_info.remote_info.remote_branch_ref, single_line=False) # Remove Rietveld metadata associated with the review branch utils.RietveldInfo.remove(branch_name=self.__branch) # Check out the review branch. We use -f in case we failed in a detached # HEAD or dirty state and want to get back to our clean branch. utils.capture_command('git', 'checkout', '-f', self.__branch, single_line=False) # This brings the review branch back to a stable state, which it was # required to be in by check_environment(). If there are no pending # changes left over from the checkout -f, this hard reset does nothing. utils.capture_command('git', 'reset', '--hard', 'HEAD', single_line=False) # If __review_branch was set, we know we have a dummy branch created # by this action which must be deleted. if self.__review_branch is not None: utils.capture_command('git', 'branch', '-D', self.__review_branch, single_line=False) if success: self.state = self.CLEAN_UP_REVIEW else: self.state = self.FINISHED self.advance()