Repository: earthlab/abc-classroom

Issue:
check for main branch before trying to change
As noted in [this comment](https://github.com/earthlab/abc-classroom/pull/335#discussion_r533625441) on PR #335, we should check whether the `master` branch exists before trying to change it. Suggested fix is:

`git show-ref --quiet --verify refs/heads/master` 

which returns an error code of 1 if the branch does not exist. 

Discussion:
lwasser: oh gosh - i wonder if CI is out of credits for earth lab too? it's not running

Patch:
diff --git a/.github/workflows/code-cov.yml b/.github/workflows/code-cov.yml
index 0f46cc3..295ef9d 100644
--- a/.github/workflows/code-cov.yml
+++ b/.github/workflows/code-cov.yml
@@ -1,5 +1,11 @@
 name: Codecov
-on: [push]
+on:
+  push:
+    branches:
+      - '*'
+  pull_request:
+    branches:
+      - '*'
 jobs:
   run:
     runs-on: ${{ matrix.os }}
@@ -15,6 +21,12 @@ jobs:
       uses: actions/setup-python@master
       with:
         python-version: 3.7
+    - name: setup git config
+      run: |
+        # Setup the git username and email, needed to run tests in test_github
+        # only local git actions, so have no impact beyong tests.
+        git config --global user.name "GitHub Actions Bot"
+        git config --global user.email "abc.actions.bot@dummy.org"
     - name: Generate coverage report
       run: |
         pip install pytest
@@ -33,4 +45,4 @@ jobs:
         name: codecov-umbrella
         fail_ci_if_error: true
         path_to_write_report: ./coverage/codecov_report.txt
-        verbose: true
\ No newline at end of file
+        verbose: true
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 40282d1..fe0b71a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -12,6 +12,7 @@ The format is based on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`
 - Overhaul fixtures, support moving  different type of files & fix codecov ci (@lwasser, #273, #172)
 - Modify how we generate an access token for GitHub API access due to upcoming deprecation of username + password authentication (@kcranston, #328)
 - Make new repositories use 'main' instead of 'master' as the default branch (@kcranston, #326)
+- Refactor the main-to-master branch renaming for better error handling and usability (@kcranston, #363)
 
 [0.1.8]
 ------------
diff --git a/README.md b/README.md
index e7aae6a..ef87222 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
 
 # Why ABC Classroom
 
-Many of us teaching data science are using GitHub Classroom as a way to teach students
+Many educators teaching data science are using GitHub Classroom as a way to teach students
 both git and GitHub skills and also potentially collaboration skills that align
 with open source software development best practices. However there are many steps
 associated with using GitHub classroom to manage a class.
diff --git a/abcclassroom/github.py b/abcclassroom/github.py
index bc8d201..d9a0b29 100644
--- a/abcclassroom/github.py
+++ b/abcclassroom/github.py
@@ -150,13 +150,14 @@ def _call_git(*args, directory=None):
             cmd,
             cwd=directory,
             check=True,
+            universal_newlines=True,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
         )
     except subprocess.CalledProcessError as e:
-        err = e.stderr.decode("utf-8")
+        err = e.stderr
         if not err:
-            err = e.stdout.decode("utf-8")
+            err = e.stdout
         raise RuntimeError(err) from e
 
     return ret
@@ -175,31 +176,6 @@ def remote_repo_exists(org, repository, token=None):
     return True
 
 
-def check_student_repo_exists(org, course, student, token=None):
-    """Check if the student has a repository for the course.
-
-    It happens that students delete their repository or do not accept the
-    invitation to the course. In either case they will not have a repository
-    yet.
-    """
-    # temporarily change log level of github3.py as it prints weird messages
-    # XXX could be done more nicely with a context manager maybe
-    gh3_log = logging.getLogger("github3")
-    old_level = gh3_log.level
-    gh3_log.setLevel("ERROR")
-
-    try:
-        g = gh3.login(token=token)
-        repository = "{}-{}".format(course, student)
-        g.repository(org, repository)
-
-    except Exception as e:
-        raise e
-
-    finally:
-        gh3_log.setLevel(old_level)
-
-
 def clone_repo(organization, repo, dest_dir):
     """Clone `repository` from `org` into a sub-directory in `directory`.
     Assumes you have ssh keys setup for github (rather than using GitHub API
@@ -307,25 +283,43 @@ def init_and_commit(directory, custom_message=False):
         print("No changes to local repository.")
 
 
-def _master_branch_to_main(directory):
+def _master_branch_to_main(dir):
     """Change the name of the master branch to main
 
     Changes the name of the master branch to main for the repo in the
     given directory. Since we create the repo on github first, which now sets
     the default branch to 'main', we need the local repo to match
-    in order to be able to push with error later.
+    in order to be able to push without errors later.
     """
-    print(
-        """Changing name of 'master' branch to 'main'
-        in repo {}""".format(
-            directory
-        )
-    )
+
     try:
-        _call_git("branch", "-m", "master", "main", directory=directory)
+        # first, verify if the  master branch exists (this is only true
+        # if there are commits on the master branch)
+        _call_git(
+            "show-ref",
+            "--quiet",
+            "--verify",
+            "refs/heads/master",
+            directory=dir,
+        )
     except RuntimeError:
-        # we get here if the master branch has already been renamed
-        pass
+        # master branch verification fails, so we check for main and create
+        # it if it does not exist
+        try:
+            _call_git(
+                "show-ref",
+                "--quiet",
+                "--verify",
+                "refs/heads/main",
+                directory=dir,
+            )
+        except RuntimeError:
+            # no main branch, create one
+            _call_git("checkout", "-b", "main", directory=dir)
+    else:
+        # rename branch
+        print("master exists, renaming")
+        _call_git("branch", "-m", "master", "main", directory=dir)
 
 
 def push_to_github(directory, branch="main"):
@@ -346,7 +340,7 @@ def pull_from_github(directory, branch="master"):
         raise e
 
 
-def git_init(directory):
+def git_init(directory, defaultbranch="main"):
     """Initialize git repository"""
     _call_git("init", directory=directory)
 
@@ -357,6 +351,31 @@ def git_init(directory):
 # about correct function.
 
 
+def check_student_repo_exists(org, course, student, token=None):
+    """Check if the student has a repository for the course.
+
+    It happens that students delete their repository or do not accept the
+    invitation to the course. In either case they will not have a repository
+    yet.
+    """
+    # temporarily change log level of github3.py as it prints weird messages
+    # XXX could be done more nicely with a context manager maybe
+    gh3_log = logging.getLogger("github3")
+    old_level = gh3_log.level
+    gh3_log.setLevel("ERROR")
+
+    try:
+        g = gh3.login(token=token)
+        repository = "{}-{}".format(course, student)
+        g.repository(org, repository)
+
+    except Exception as e:
+        raise e
+
+    finally:
+        gh3_log.setLevel(old_level)
+
+
 def close_existing_pullrequests(
     org, repository, branch_base="new-material-", token=None
 ):


Test patch:
diff --git a/.github/workflows/run-tests-mac-win-linux.yml b/.github/workflows/run-tests-mac-win-linux.yml
index 227d1d8..08cf593 100644
--- a/.github/workflows/run-tests-mac-win-linux.yml
+++ b/.github/workflows/run-tests-mac-win-linux.yml
@@ -5,10 +5,10 @@ name: Matrix OS / Python Version - Run Tests & Build Docs
 
 on:
   push:
-    branches: 
+    branches:
       - '*'
   pull_request:
-    branches: 
+    branches:
       - '*'
 jobs:
   setup-build:
@@ -31,6 +31,13 @@ jobs:
         python -m pip install --upgrade pip
         pip install -r dev-requirements.txt
         pip install -e .
+    - name: setup git config
+      run: |
+        # Setup the git username and email, needed to run tests in test_github
+        # only local git actions, so have no impact beyong tests.
+        git config --global user.name "GitHub Actions Bot"
+        git config --global user.email "abc.actions.bot@dummy.org"
+
     - name: Test with pytest
       run: |
         pytest
diff --git a/abcclassroom/tests/test_github.py b/abcclassroom/tests/test_github.py
new file mode 100644
index 0000000..8a3d2b0
--- /dev/null
+++ b/abcclassroom/tests/test_github.py
@@ -0,0 +1,60 @@
+# Tests for git and github methods
+from pathlib import Path
+
+import abcclassroom.github as abcgit
+
+
+def test_init_and_commit(default_config, tmp_path):
+    """
+    Tests that we can create a directory, initialize it as a git repo,
+    and commit some changes.
+    """
+    repo_dir = Path(tmp_path, "init-and-commit")
+    repo_dir.mkdir()
+    a_file = Path(repo_dir, "testfile.txt")
+    a_file.write_text("Some text")
+    abcgit.init_and_commit(repo_dir)
+    assert Path(repo_dir, ".git").exists()
+    git_return = abcgit._call_git("log", directory=repo_dir)
+    assert git_return.stdout.startswith("commit")
+
+
+def test_master_branch_to_main_repeatedly(tmp_path):
+    """
+    Tests that we can sucessfully change the default master branch to
+    main, and nothing bad happends if we try and do it again
+    """
+    repo_dir = Path(tmp_path, "change-master")
+    repo_dir.mkdir()
+    abcgit.git_init(repo_dir)
+
+    # change the default branch name
+    abcgit._master_branch_to_main(repo_dir)
+    # in order to test that main exists, we need to add some commits
+    a_file = Path(repo_dir, "testfile.txt")
+    a_file.write_text("Some text")
+    commit_msg = "commit some things"
+    abcgit.commit_all_changes(repo_dir, msg=commit_msg)
+
+    # now test the existance of main
+    abcgit._call_git(
+        "show-ref",
+        "--quiet",
+        "--verify",
+        "refs/heads/main",
+        directory=repo_dir,
+    )
+
+    # trying again should also work without error
+    abcgit._master_branch_to_main(repo_dir)
+
+
+def test_master_branch_to_main_no_commits(tmp_path):
+    """
+    Tests that changing the name of the master branch in a initialized
+    repo without commits works.
+    """
+    repo_dir = Path(tmp_path, "change-master-no-commits")
+    repo_dir.mkdir()
+    abcgit.git_init(repo_dir)
+    abcgit._master_branch_to_main(repo_dir)