From ebe77391c67c5fa6314697c01af103e3722b917c Mon Sep 17 00:00:00 2001
From: Stefan Zweifel <stefanzweifel@users.noreply.github.com>
Date: Sun, 27 Feb 2022 07:52:17 +0100
Subject: [PATCH] Add `create_branch` option to force create a new branch
 (#203)

* Add create_branch option

* Checkout new branch if create_branch input is true

* Add tests

* Update README
---
 README.md                  |  28 ++--
 action.yml                 |   5 +-
 entrypoint.sh              |  14 +-
 tests/git-auto-commit.bats | 257 +++++++++++++++++++++++++++++++++++++
 4 files changed, 287 insertions(+), 17 deletions(-)

diff --git a/README.md b/README.md
index f18675b..e70737e 100644
--- a/README.md
+++ b/README.md
@@ -28,15 +28,16 @@ The following is an extended example with all possible options available for thi
 ```yaml
 - uses: stefanzweifel/git-auto-commit-action@v4
   with:
-    # Optional, but recommended
+    # Optional. Commit message for the created commit.
     # Defaults to "Apply automatic changes"
     commit_message: Automated Change
 
-    # Optional branch name where commit should be pushed to.
-    # Defaults to the current branch.
+    # Optional. Local and remote branch name where commit is going to be pushed
+    #  to. Defaults to the current branch.
+    #  You might need to set `create_branch: true` if the branch does not exist.
     branch: feature-123
 
-    # Optional. Used by `git-commit`.
+    # Optional. Options used by `git-commit`.
     # See https://git-scm.com/docs/git-commit#_options
     commit_options: '--no-verify --signoff'
 
@@ -47,8 +48,8 @@ The following is an extended example with all possible options available for thi
     # - https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefpathspecapathspec
     file_pattern: src/*.js tests/*.js *.php
 
-    # Optional local file path to the repository
-    # Defaults to the root of the repository
+    # Optional. Local file path to the repository.
+    # Defaults to the root of the repository.
     repository: .
 
     # Optional commit user and author settings
@@ -56,19 +57,19 @@ The following is an extended example with all possible options available for thi
     commit_user_email: my-github-actions-bot@example.org # defaults to "actions@github.com"
     commit_author: Author <actions@github.com> # defaults to author of the commit that triggered the run
 
-    # Optional tag message 
-    # Action will create and push a new tag to the remote repository and the defined branch
+    # Optional. Tag name being created in the local repository and 
+    # pushed to remtoe repository and defined branch.
     tagging_message: 'v1.0.0'
 
-    # Optional. Used by `git-status`
-    # See https://git-scm.com/docs/git-status#_options
+    # Optional. Option used by `git-status` to determine if the repository is 
+    # dirty. See https://git-scm.com/docs/git-status#_options
     status_options: '--untracked-files=no'
 
-    # Optional. Used by `git-add`
+    # Optional. Options used by `git-add`.
     # See https://git-scm.com/docs/git-add#_options
     add_options: '-u'
 
-    # Optional. Used by `git-push`
+    # Optional. Options used by `git-push`.
     # See https://git-scm.com/docs/git-push#_options
     push_options: '--force'
     
@@ -84,6 +85,9 @@ The following is an extended example with all possible options available for thi
     # Optional. Prevents the shell from expanding filenames. 
     # Details: https://www.gnu.org/software/bash/manual/html_node/Filename-Expansion.html
     disable_globbing: true
+
+    # Optional. Create given branch name in local and remote repository.
+    create_branch: true
 ```
 
 Please note that the Action depends on `bash`. If you're using the Action in a job in combination with a custom Docker container, make sure that `bash` is installed.
diff --git a/action.yml b/action.yml
index 03a8557..0c30824 100644
--- a/action.yml
+++ b/action.yml
@@ -25,7 +25,7 @@ inputs:
     required: false
     default: ''
   file_pattern:
-    description: File pattern used for `git add`. For example `src/\*.js`
+    description: File pattern used for `git add`. For example `src/*.js`
     required: false
     default: '.'
   repository:
@@ -67,6 +67,9 @@ inputs:
   disable_globbing:
     description: Stop the shell from expanding filenames (https://www.gnu.org/software/bash/manual/html_node/Filename-Expansion.html)
     default: false
+  create_branch:
+    description: Create new branch with the name of `branch`-input in local and remote repository, if it doesn't exist yet.
+    default: false
 
 outputs:
   changes_detected:
diff --git a/entrypoint.sh b/entrypoint.sh
index 3b0c85e..dee54a9 100755
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -48,19 +48,25 @@ _switch_to_branch() {
     echo "INPUT_BRANCH value: $INPUT_BRANCH";
 
     # Fetch remote to make sure that repo can be switched to the right branch.
-
     if "$INPUT_SKIP_FETCH"; then
         echo "::debug::git-fetch has not been executed";
     else
         git fetch --depth=1;
     fi
 
+    # If `skip_checkout`-input is true, skip the entire checkout step.
     if "$INPUT_SKIP_CHECKOUT"; then
         echo "::debug::git-checkout has not been executed";
     else
-        # Switch to branch from current Workflow run
-        # shellcheck disable=SC2086
-        git checkout $INPUT_BRANCH --;
+        # Create new local branch if `create_branch`-input is true
+        if "$INPUT_CREATE_BRANCH"; then
+            # shellcheck disable=SC2086
+            git checkout -B $INPUT_BRANCH --;
+        else
+            # Switch to branch from current Workflow run
+            # shellcheck disable=SC2086
+            git checkout $INPUT_BRANCH --;
+        fi
     fi
 }
 
diff --git a/tests/git-auto-commit.bats b/tests/git-auto-commit.bats
index 2250496..da25586 100644
--- a/tests/git-auto-commit.bats
+++ b/tests/git-auto-commit.bats
@@ -26,6 +26,7 @@ setup() {
     export INPUT_SKIP_FETCH=false
     export INPUT_SKIP_CHECKOUT=false
     export INPUT_DISABLE_GLOBBING=false
+    export INPUT_CREATE_BRANCH=false
 
     # Configure Git
     if [[ -z $(git config user.name) ]]; then
@@ -533,3 +534,259 @@ git_auto_commit() {
     assert_line "::set-output name=changes_detected::true"
     assert_line "::debug::Push commit to remote branch dev"
 }
+
+@test "script fails to push commit to new branch that does not exist yet" {
+    INPUT_BRANCH="not-existend-branch"
+    INPUT_CREATE_BRANCH=false
+
+    run git branch
+    refute_line --partial "not-existend-branch"
+
+    run git branch -r
+    refute_line --partial "origin/not-existend-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_failure
+
+    assert_line "INPUT_REPOSITORY value: ${INPUT_REPOSITORY}"
+    assert_line "::set-output name=changes_detected::true"
+    assert_line "INPUT_BRANCH value: not-existend-branch"
+    assert_line "fatal: invalid reference: not-existend-branch"
+
+    run git branch
+    refute_line --partial "not-existend-branch"
+
+    run git branch -r
+    refute_line --partial "origin/not-existend-branch"
+}
+
+@test "It creates new local branch and pushes the new branch to remote" {
+    INPUT_BRANCH="not-existend-branch"
+    INPUT_CREATE_BRANCH=true
+
+    run git branch
+    refute_line --partial "not-existend-branch"
+
+    run git branch -r
+    refute_line --partial "origin/not-existend-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_success
+
+    assert_line "INPUT_REPOSITORY value: ${INPUT_REPOSITORY}"
+    assert_line "::set-output name=changes_detected::true"
+    assert_line -e "::set-output name=commit_hash::[0-9a-f]{40}$"
+    assert_line "INPUT_BRANCH value: not-existend-branch"
+    assert_line "INPUT_FILE_PATTERN: ."
+    assert_line "INPUT_COMMIT_OPTIONS: "
+    assert_line "::debug::Apply commit options "
+    assert_line "INPUT_TAGGING_MESSAGE: "
+    assert_line "No tagging message supplied. No tag will be added."
+    assert_line "INPUT_PUSH_OPTIONS: "
+    assert_line "::debug::Apply push options "
+    assert_line "::debug::Push commit to remote branch not-existend-branch"
+
+    run git branch
+    assert_line --partial "not-existend-branch"
+
+    run git branch -r
+    assert_line --partial "origin/not-existend-branch"
+}
+
+@test "it does not create new local branch if local branch already exists" {
+
+    git checkout -b not-existend-remote-branch
+    git checkout master
+
+    INPUT_BRANCH="not-existend-remote-branch"
+    INPUT_CREATE_BRANCH=true
+
+    run git branch
+    assert_line --partial "not-existend-remote-branch"
+
+    run git branch -r
+    refute_line --partial "origin/not-existend-remote-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_success
+
+    assert_line "INPUT_REPOSITORY value: ${INPUT_REPOSITORY}"
+    assert_line "::set-output name=changes_detected::true"
+    assert_line -e "::set-output name=commit_hash::[0-9a-f]{40}$"
+    assert_line "INPUT_BRANCH value: not-existend-remote-branch"
+    assert_line "INPUT_FILE_PATTERN: ."
+    assert_line "INPUT_COMMIT_OPTIONS: "
+    assert_line "::debug::Apply commit options "
+    assert_line "INPUT_TAGGING_MESSAGE: "
+    assert_line "No tagging message supplied. No tag will be added."
+    assert_line "INPUT_PUSH_OPTIONS: "
+    assert_line "::debug::Apply push options "
+    assert_line "::debug::Push commit to remote branch not-existend-remote-branch"
+
+    run git branch
+    assert_line --partial "not-existend-remote-branch"
+
+    run git branch -r
+    assert_line --partial "origin/not-existend-remote-branch"
+}
+
+@test "it creates new local branch and pushes branch to remote even if the remote branch already exists" {
+
+    # Create `existing-remote-branch` on remote with changes the local repository does not yet have
+    cd $FAKE_TEMP_LOCAL_REPOSITORY;
+    git checkout -b "existing-remote-branch"
+    touch new-branch-file.txt
+    git add new-branch-file.txt
+    git commit -m "Add additional file";
+    git push origin existing-remote-branch;
+
+    run git branch;
+    assert_line --partial "existing-remote-branch"
+
+    # ---------
+    # Switch to our regular local repository and run `git-auto-commit`
+    cd $FAKE_LOCAL_REPOSITORY;
+
+    INPUT_BRANCH="existing-remote-branch"
+    INPUT_CREATE_BRANCH=true
+
+    run git branch
+    refute_line --partial "existing-remote-branch"
+
+    run git fetch --all;
+    run git pull origin existing-remote-branch;
+    run git branch -r;
+    assert_line --partial "origin/existing-remote-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_success
+
+    assert_line "INPUT_REPOSITORY value: ${INPUT_REPOSITORY}"
+    assert_line "::set-output name=changes_detected::true"
+    assert_line -e "::set-output name=commit_hash::[0-9a-f]{40}$"
+    assert_line "INPUT_BRANCH value: existing-remote-branch"
+    assert_line "INPUT_FILE_PATTERN: ."
+    assert_line "INPUT_COMMIT_OPTIONS: "
+    assert_line "::debug::Apply commit options "
+    assert_line "INPUT_TAGGING_MESSAGE: "
+    assert_line "No tagging message supplied. No tag will be added."
+    assert_line "INPUT_PUSH_OPTIONS: "
+    assert_line "::debug::Apply push options "
+    assert_line "::debug::Push commit to remote branch existing-remote-branch"
+
+    run git branch
+    assert_line --partial "existing-remote-branch"
+
+    run git branch -r
+    assert_line --partial "origin/existing-remote-branch"
+
+    # Assert that branch "existing-remote-branch" was updated on remote
+    current_sha="$(git rev-parse --verify --short existing-remote-branch)"
+    remote_sha="$(git rev-parse --verify --short origin/existing-remote-branch)"
+
+    assert_equal $current_sha $remote_sha;
+}
+
+@test "script fails if new local branch is checked out and push fails as remote has newer commits than local" {
+    # Create `existing-remote-branch` on remote with changes the local repository does not yet have
+    cd $FAKE_TEMP_LOCAL_REPOSITORY;
+    git checkout -b "existing-remote-branch"
+    touch new-branch-file.txt
+    git add new-branch-file.txt
+    git commit -m "Add additional file";
+    git push origin existing-remote-branch;
+
+    run git branch;
+    assert_line --partial "existing-remote-branch"
+
+    # ---------
+    # Switch to our regular local repository and run `git-auto-commit`
+    cd $FAKE_LOCAL_REPOSITORY;
+
+    INPUT_BRANCH="existing-remote-branch"
+    INPUT_CREATE_BRANCH=true
+
+    run git branch
+    refute_line --partial "existing-remote-branch"
+
+    run git fetch --all;
+    run git branch -r;
+    assert_line --partial "origin/existing-remote-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_failure
+
+    assert_line "hint: Updates were rejected because the tip of your current branch is behind"
+
+    # Assert that branch exists locally and on remote
+    run git branch
+    assert_line --partial "existing-remote-branch"
+
+    run git branch -r
+    assert_line --partial "origin/existing-remote-branch"
+
+    # Assert that branch "existing-remote-branch" was not updated on remote
+    current_sha="$(git rev-parse --verify --short existing-remote-branch)"
+    remote_sha="$(git rev-parse --verify --short origin/existing-remote-branch)"
+
+    refute [assert_equal $current_sha $remote_sha];
+}
+
+@test "It pushes commit to remote if branch already exists and local repo is behind its remote counterpart" {
+    # Create `new-branch` on remote with changes the local repository does not yet have
+    cd $FAKE_TEMP_LOCAL_REPOSITORY;
+
+    git checkout -b "new-branch"
+    touch new-branch-file.txt
+    git add new-branch-file.txt
+
+    git commit --quiet -m "Add additional file";
+    git push origin new-branch;
+
+    run git branch -r
+    assert_line --partial "origin/new-branch"
+
+    # ---------
+    # Switch to our regular local repository and run `git-auto-commit`
+    cd $FAKE_LOCAL_REPOSITORY;
+
+    INPUT_BRANCH="new-branch"
+
+    # Assert that local remote does not know have "new-branch" locally nor does
+    # know about the remote branch.
+    run git branch
+    refute_line --partial "new-branch"
+
+    run git branch -r
+    refute_line --partial "origin/new-branch"
+
+    touch "${FAKE_LOCAL_REPOSITORY}"/new-file-{1,2,3}.txt
+
+    run git_auto_commit
+
+    assert_success
+
+    assert_line "INPUT_BRANCH value: new-branch"
+    assert_line --partial "::debug::Push commit to remote branch new-branch"
+
+    # Assert that branch "new-branch" was updated on remote
+    current_sha="$(git rev-parse --verify --short new-branch)"
+    remote_sha="$(git rev-parse --verify --short origin/new-branch)"
+
+    assert_equal $current_sha $remote_sha;
+}