
When we talk about automating the development and testing process, we mean that this is a very large-scale action, and it really is. And if we decompose it in parts, then separate fragments of the whole picture will become visible - this fragmentation of the process is very important in two cases:
- actions are performed manually, which requires concentration and accuracy;
- hard time frame.
In our case, there is a time limit: releases are formed, tested and rolled out to the production server twice a day. With limited deadlines in the life cycle of a release, the process of removing (rolling back) from the release branch of a task containing an error is important. To do this, we use git rebase. Since git rebase is a completely manual operation that requires attentiveness and thoroughness and takes a long time, we have automated the process of deleting a task from the release branch.

Git flow
At the moment, Git is one of the most common version control systems, and we successfully use it in Badoo.
The process of working with Git is quite simple.

The peculiarity of our model is that we develop and test every task in a separate branch. The name of this branch consists of a ticket number in JIRA and a free description of the task. For example:
BFG-9000_All_developers_should_be_given_a_years_holiday_ (paid)
We collect the release and test it from a separate branch (release), into which completed and tested tasks on a devel environment are merged. Since we post the code to the production server twice a day, we create two new release branches every day, respectively.
')

The release is formed by merging tasks into the release branch using the automerge tool. We also have a master branch, which is a copy of the production server. After the integration testing stage of the release and each individual task, the code is sent to the production server and merged into the master branch.
When a release is tested on a staging environment and an error is detected in one of the tasks, and there is no time to fix it, we simply remove this task from the release using git rebase.
Note. We do not use the git revert function in the release branch, because if you delete the task from the release branch with git revert and the release branch merges into master, from which the developer then pulls the fresh code into the branch where the error occurred, he will have to revert to revert to return your changes.
At the next stage, we compile a new version of the release, roll it out to the staging environment, check for errors, run autotests and, with a positive result, upload the code to the production server.
The main points of this scheme are fully automated and work in the process of continuous integration (until now, only the removal of the task from the release branch was done manually).


Formulation of the problem
Consider what can be used to automate the process:
1. The release branch, from which we are going to roll back the ticket, consists of commits of two categories:
- The merge commit, which is obtained when merging a task branch into a release branch, contains the name of the ticket in the commit message, since the branches are named with the task prefix;
- merge commit, which is obtained by merging the master branch into the release branch in automatic mode. On the master, we apply patches in semi-automatic mode through our special tool DeployDashboard. The patches are attached to the corresponding ticket, while the commit message indicates the number of this ticket and the description of the patch.
2. Built-in git rebase tool, which is best used interactively thanks to convenient visualization.
Problems you may encounter :
1. When the git rebase operation is performed, all commits in the branch are overclocked, starting with the one that is rolled back.
2. If during the formation of a branch any merge conflict was resolved manually, then Git will not save the solution of this conflict in memory, therefore, when performing the git rebase operation, you will need to re-fix the merge conflicts manually.
3. Conflicts in a particular algorithm are divided into two types:
- simple - such conflicts arise due to the fact that the functionality of the version control system does not allow memorizing previously resolved merge conflicts;
- complex - arise due to the fact that the code was corrected in a particular line (file) not only in the commit that is removed from the branch, but also in subsequent commits, which are peremerzhivayutsya in the git rebase process. At the same time, the developer corrected this conflict manually and performed a push to the release branch.
Git has an interesting feature called git rerere, which remembers the resolution of conflicts when merzh. It turns on automatically, but, unfortunately, cannot help us in this case. This function only works when there are two long-lived branches that are constantly merging - Git remembers such conflicts without problems.
We have only one branch, and if the -force function is not used when performing git push changes to the repository, then after each git rebase you will have to create a new branch with a new trunk of changes. For example, we write the postfix _r1, r2, r3 ... after each successful git rebase operation and execute git push of the new release branch to the repository. Thus, the history of conflict resolution is not saved.
What do we want to end up with ?
By pressing a certain button in our bugtracker:
1. The task will be automatically removed from the release.
2. A new release branch will be created.
3. The status of the task will be transferred to Reopen.
4. In the process of removing the task from the release, all simple merge conflicts will be resolved.
Unfortunately, in any of the schemes it is impossible to resolve complex merge conflicts, so if such a conflict arises, we will notify the developer and the release engineer.

Main functions
1. Our script uses interactive rebase and catches commits with the number of the task to be rolled back in the release branch.
2. When finding the necessary commits, it deletes them, while remembering the names of the files that were changed in them.
3. Next, he peremerzhivaet all commits, starting with the last remote in the trunk of the branch.
4. If a conflict occurs, it checks the files that are involved in the conflict. If these files coincide with the files of remote committees, then we notify the developer and release engineer that a complex conflict has arisen that needs to be resolved manually.
5. If the files do not match, but a conflict has occurred, then this is a simple conflict. Then we take the code of files from the commit in which the developer has already solved this conflict, from the origin repository.
So "run to the head of the branch."
The probability that we will end up in a complex conflict is negligible, that is, 99% of the process’s accomplishments will be automatic.

Implementation
Now let's take a look at what our script will do (the example uses only automatic rebase and you can use the script just in the console):
1. Clear the repository and pull out the latest version of the release branch.
2. We get the top commit in the trunk with the merging into the release of the branch that we want to roll back.
but. If there is no commit, then we inform you that there is nothing to roll back.
3. We generate a script editor, which only removes merge commies hashes from the trunk, thus removing them from the history.
4. In the environment of the script-inverter set the script editor (EDITOR), which we generated in the previous step.
5. Run git rebase -ip for release. Check the error code.
but. If 0, then everything went well. Go to step 2 to find possible previous commits of the deleted branch of the task.
b. If not 0, then a conflict has occurred. We are trying to solve:
i. We memorize the commit hash that could not be imposed.
It is in the .git / rebase-merge / stopped-sha file.
ii. Parse the output of the rebase command to find out what is wrong.
1. If Git tells us “CONFLICT (content): Merge conflict in”, then we compare this file with the previous revision from the deleted one, and if it does not differ (the file did not change in the commit), then simply take this file from the head of the build branch and commit If it is different, then we exit, and the developer resolves the conflict manually.
2. If Git says “fatal: Commit is a merge but no-m option was given”, then just repeat the rebase with the - continue flag. The merge commit will be skipped, but the changes will not be lost. This usually happens with the master branch, but it has already been pulled to the branch head and this merge commit is not needed.
3. If Git says “error: couldn’t apply ...” When you have resolved this problem run “git rebase - continue”, then do git status to get a list of files. If at least one file from the status is in the commit that we roll back, then skip the commit (rebase --skip), which we memorized at step 5.bi, writing about this in the log so that the release engineer will see it and decide if this is necessary commit or not.
4. If none of the above happened, then exit the script and say that something inexplicable happened.
6. Repeat point 5 until exit code 0 appears at the output, or the counter in the cycle will not be> 5 to avoid looping errors.
Script code function runBuildRevert($args) { if (count($args) != 2) { $this->commandUsage("<build-name> <ticket-key>"); return $this->error("Unknown build!");; } $build_name = array_shift($args); $ticket_key = array_shift($args); $build = $this->Deploy->buildForNameOrBranch($build_name); if (!$build) return false; if ($this->directSystem("git reset --hard && git clean -fdx")) { return $this->error("Can't clean directory!"); } if ($this->directSystem("git fetch")) { return $this->error("Can't fetch from origin!"); } if ($this->directSystem("git checkout " . $build['branch_name'])) { return $this->error("Can't checkout build branch!"); } if ($this->directSystem("git pull origin " . $build['branch_name'])) { return $this->error("Can't pull build branch!"); } $commit = $this->_getTopBranchToBuildMergeCommit($build['branch_name'], $ticket_key); $in_stream_count = 0; while (!empty($commit)) { $in_stream_count += 1; if ($in_stream_count >= 5) return $this->error("Seems rebase went to infinite loop!"); $editor = $this->_generateEditor($build['branch_name'], $ticket_key); $output = ''; $code = 0; $this->exec( 'git rebase -ip ' . $commit . '^^', $output, $code, false ); while ($code) { $output = implode("\n", $output); $conflicts_result = $this->_resolveRevertConflicts($output, $build['branch_name'], $commit); if (self::FLAG_REBASE_STOP !== $conflicts_result) { $command = '--continue'; if (self::FLAG_REBASE_SKIP === $conflicts_result) { $command = '--skip'; } $output = ''; $code = 0; $this->exec( 'git rebase ' . $command, $output, $code, false ); } else { unlink($editor); return $this->error("Giving up, can't resolve conflicts! Do it manually.. Output was:\n" . var_export($output, 1)); } } unlink($editor); $commit = $this->_getTopBranchToBuildMergeCommit($build['branch_name'], $ticket_key); } if (empty($in_stream_count)) return $this->error("Can't find ticket merge in branchdiff with master!"); return true; } protected function _resolveRevertConflicts($output, $build_branch, $commit) { $res = self::FLAG_REBASE_STOP; $stopped_sha = trim(file_get_contents('.git/rebase-merge/stopped-sha')); if (preg_match_all('/^CONFLICT\s\(content\)\:\sMerge\sconflict\sin\s(.*)$/m', $output, $m)) { $conflicting_files = $m[1]; foreach ($conflicting_files as $file) { $output = ''; $this->exec( 'git diff ' . $commit . '..' . $commit . '^ -- ' . $file, $output ); if (empty($output)) { $this->exec('git show ' . $build_branch . ':' . $file . ' > ' . $file); $this->exec('git add ' . $file); $res = self::FLAG_REBASE_CONTINUE; } else { return $this->error("Can't resolve conflict, because file was changed in reverting branch!"); } } } elseif (preg_match('/fatal\:\sCommit\s' . $stopped_sha . '\sis\sa\smerge\sbut\sno\s\-m\soption\swas\sgiven/m', $output)) { $res = self::FLAG_REBASE_CONTINUE; } elseif (preg_match('/error\:\scould\snot\sapply.*When\syou\shave\sresolved\sthis\sproblem\srun\s"git\srebase\s\-\-continue"/sm', $output)) { $files_status = ''; $this->exec( 'git status -s|awk \'{print $2;}\'', $files_status ); foreach ($files_status as $file) { $diff_in_reverting = ''; $this->exec( 'git diff ' . $commit . '..' . $commit . '^ -- ' . $file, $diff_in_reverting ); if (!empty($diff_in_reverting)) { $this->warning("Skipping commit " . $stopped_sha . " because it touches files we are reverting!"); $res = self::FLAG_REBASE_SKIP; break; } } } return $res; } protected function _getTopBranchToBuildMergeCommit($build_branch, $ticket) { $commit = ''; $this->exec( 'git log ' . $build_branch . ' ^origin/master --merges --grep ' . $ticket . ' -1 --pretty=format:%H', $commit ); return array_shift($commit); } protected function _generateEditor($build_branch, $ticket, array $exclude_commits = array()) { $filename = PHPWEB_PATH_TEMPORARY . uniqid($build_branch) . '.php'; $content = <<<'CODE'

Conclusion
As a result, we received a script that deletes the task from the release branch in automatic mode. We saved time in the process of forming and testing the release, while almost completely eliminating the human factor.
Of course, our script is not suitable for all Git users. In some cases it is easier to use git revert, but it’s better not to get carried away (revert to revert to revert ...). We hope that not the easiest git rebase operation has become more understandable to you, and those who constantly use git rebase in the development and release generation process will also use our script.
Ilya Ageev , QA Lead and
Vladislav Chernov , Release engineer