Chapter 2 : Code versioning using Git#
In this chapter, we will take a look at how Git works and how to use it to version code.
Note
The content of this chapter has been adapted from a tutorial made by Alexandre Abadie and Kim Tâm Huynh from the INRIA SED of Paris.
Getting started#
First you will start a small Python project
Initialize the project#
Create and initialize a repository#
Create a directory dummymaths
:
! mkdir dummymaths
%cd dummymaths
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
Warning
If you are running this notebook on Collab, or if you are using an old version of Git, you need to run the following cell which will make sure your default branch is nammed main
and not master
as this default was changed a couple years ago.
Otherwise, you would have to change main
to master
manually in all the commands of this notebook.
! git config --global init.defaultBranch main
Initialize a Git local repository in the dummymaths
directory with the git init command:
! git init
Initialized empty Git repository in /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/.git/
Configure your name and email#
Configure your git account using simple configuration files 3 levels:
local :
<local copy>/.git/config
, by defaultglobal :
~/.gitconfig
, option--global
system :
/etc/gitconfig
, option--system
To configure your user name and email address, you can either edit the config files directly, or use the git config command:
! git config --local user.name "John Doe"
! git config --local user.email john.doe@inria.fr
Add a simple text file#
Create a README.md
file and then check the state of your local copy:
! echo "This is the README file" > README.md
! git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
nothing added to commit but untracked files present (use "git add" to track)
Add the README.md
file to the staging area and check again the state of your local copy:
! git add README.md
! git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
Edit the README.md
file (for example, add a title, a short description of this small Python project), save and check again the state of your local copy.
! echo "This is the new README file" > README.md
You should have changes both in the staging area and in the working directory (local changes). We can display the changes between HEAD and the staging area:
! git diff --staged
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7dcf35b
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+This is the README file
And the changes between the staging area and the workspace:
! git diff
diff --git a/README.md b/README.md
index 7dcf35b..ed6b542 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-This is the README file
+This is the new README file
The outputs of these commands with the green and red lines are what is commonly called “a diff”. Diffs are everywhere in Git and it is very important to get used to reading them. When reviewing Pull Requests on Github or Gitlab for example, you usually look at the diff.
You can read the following article to get more details on how to read them. After practicing a bit, this will become very natural.
In order to follow the tutorial, remember that a diff should be red at the line level: lines in red with a -
symbol at the begining are lines which were removed, while green lines with a +
symbol are lines which were added.
Commit all changes in the README.md
file (both in staging and local) and check one last time the state of the local copy:
! git add README.md # staging
! git commit -m "initial commit" # local
! git status
[main (root-commit) ceb8dd5] initial commit
1 file changed, 1 insertion(+)
create mode 100644 README.md
On branch main
nothing to commit, working tree clean
Add a python file to the project#
Add the file myfuncs.py
with the following content:
%%writefile myfuncs.py
"""Some useless mathematical utility functions."""
def add(a, b):
"""Return the sum of a and b."""
return a + b
def sub(a, b):
"""Substract b from a."""
return a - b
Writing myfuncs.py
Commit this file:
! git add myfuncs.py
! git commit -m "initial version of myfuncs.py"
[main d557837] initial version of myfuncs.py
1 file changed, 10 insertions(+)
create mode 100644 myfuncs.py
Add a testing file with pytest#
Add the file test_myfuncs.py
with the following content:
%%writefile test_myfuncs.py
import pytest
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 42),
(42, 0, 42),
(42, -42, 0),
(-42, 42, 0),
]
)
def test_add(a, b, res):
from myfuncs import add
assert add(a, b) == res
Writing test_myfuncs.py
Use pytest
to run the tests, verify
that they pass and then commit test_myfuncs.py
(and only this one!):
! pytest test_myfuncs.py
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 5 items
test_myfuncs.py ..... [100%]
============================== 5 passed in 0.01s ===============================
Note that you can use the verbose option (-v
) to have more information:
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 5 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 20%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 40%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 60%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 80%]
test_myfuncs.py::test_add[-42-42-0] PASSED [100%]
============================== 5 passed in 0.01s ===============================
Let’s commit these tests:
! git add test_myfuncs.py
! git commit -m "tests: add test function for add"
[main d5d3c04] tests: add test function for add
1 file changed, 17 insertions(+)
create mode 100644 test_myfuncs.py
Ignore generated files#
At this stage, they are Python bytecode generated files displayed when running git status:
! git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
__pycache__/
nothing added to commit but untracked files present (use "git add" to track)
We don’t want to commit them inadvertently: this is the purpose of the .gitignore file.
The .gitignore file is basically a list of patterns for files to ignore. Here, we want to ignore all files ending with pyc
:
! echo "*pyc" > .gitignore
Check that bytecode generated files are not listed anymore when running git status:
! git status
On branch main
Untracked files:
(use "git add <file>..." to include in what will be committed)
.gitignore
nothing added to commit but untracked files present (use "git add" to track)
The Python bytecode files do not appear in the ouput, but the .gitignore file does.
Let’s add it and commit it as we wish to version it together with our code:
! git add .gitignore
! git commit -m "ignore Python generated files"
[main fcd5975] ignore Python generated files
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
Manage the changes#
Let’s continue working on our dummymaths
Python project!
Visualize the project’s history#
First, we would like to refresh our mind on the latest modifications that were made to the project. Git is your friend and provides you the git log command to display the history of changes already committed.
Peaceful commands
As for git status, this command can never hurt you and will probably save you a lot by reminding you the project’s history, so do not hesitate to use it!
Without any argument, git log displays all the commits of the project in reverse chronological order (the first commit is the most recent commit):
! git log
commit fcd59755c8f205e887bc502886e5dccacee07932 (HEAD -> main)
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:41:30 2023 +0100
ignore Python generated files
commit d5d3c04553c252334236ef07322675e1f90451ca
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:58 2023 +0100
tests: add test function for add
commit d557837cf6a8c1a047acfbc739ab1629612f8d64
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:41 2023 +0100
initial version of myfuncs.py
commit ceb8dd57bd36b9f7ee95fbac84dd1d2c62a7b860
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:35 2023 +0100
initial commit
You can configure this command in a lot of ways that we cannot cover here. Do not hesitate to take a look at the documentation, you will likely find pretty cool ways to visualize a project’s history.
As an example, the -some_integer
option lets you control the number of commits you want to display:
! git log -2
commit fcd59755c8f205e887bc502886e5dccacee07932 (HEAD -> main)
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:41:30 2023 +0100
ignore Python generated files
commit d5d3c04553c252334236ef07322675e1f90451ca
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:58 2023 +0100
tests: add test function for add
The -p
option prints the differences between the commits:
! git log -2 -p
commit fcd59755c8f205e887bc502886e5dccacee07932 (HEAD -> main)
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:41:30 2023 +0100
ignore Python generated files
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..72723e5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*pyc
commit d5d3c04553c252334236ef07322675e1f90451ca
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:58 2023 +0100
tests: add test function for add
diff --git a/test_myfuncs.py b/test_myfuncs.py
new file mode 100644
index 0000000..97539fb
--- /dev/null
+++ b/test_myfuncs.py
@@ -0,0 +1,17 @@
+
+import pytest
+
+@pytest.mark.parametrize(
+ "a,b,res",
+ [
+ (0, 0, 0),
+ (0, 42, 42),
+ (42, 0, 42),
+ (42, -42, 0),
+ (-42, 42, 0),
+ ]
+)
+def test_add(a, b, res):
+ from myfuncs import add
+
+ assert add(a, b) == res
Undoing simple mistakes#
Let’s first extend the tests in test_myfuncs.py
with a test for the sub
function:
%%writefile -a test_myfuncs.py
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, -42),
(42, 0, 42),
(42, 42, 1),
(42, -42, 84),
(-42, 42, -84),
]
)
def test_sub(a, b, res):
from myfuncs import sub
assert sub(a, b) == res
Appending to test_myfuncs.py
We are happy with our test and we are in a rush so we add and commit our changes right away:
! git add test_myfuncs.py
! git commit -m "add test function for sub"
[main 2541e03] add test function for sub
1 file changed, 16 insertions(+)
But wait, we aren’t as good mathematicians as we thought and we made a mistake:
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 11 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 9%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 18%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 27%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 36%]
test_myfuncs.py::test_add[-42-42-0] PASSED [ 45%]
test_myfuncs.py::test_sub[0-0-0] PASSED [ 54%]
test_myfuncs.py::test_sub[0-42--42] PASSED [ 63%]
test_myfuncs.py::test_sub[42-0-42] PASSED [ 72%]
test_myfuncs.py::test_sub[42-42-1] FAILED [ 81%]
test_myfuncs.py::test_sub[42--42-84] PASSED [ 90%]
test_myfuncs.py::test_sub[-42-42--84] PASSED [100%]
=================================== FAILURES ===================================
______________________________ test_sub[42-42-1] _______________________________
a = 42, b = 42, res = 1
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, -42),
(42, 0, 42),
(42, 42, 1),
(42, -42, 84),
(-42, 42, -84),
]
)
def test_sub(a, b, res):
from myfuncs import sub
> assert sub(a, b) == res
E assert 0 == 1
E + where 0 = <function sub at 0x7f9660d3e290>(42, 42)
test_myfuncs.py:33: AssertionError
=========================== short test summary info ============================
FAILED test_myfuncs.py::test_sub[42-42-1] - assert 0 == 1
========================= 1 failed, 10 passed in 0.09s =========================
Obviously, 42 - 42
is equal to 0 and not 1 !
Now, we could change the test file to correct that and make a new commit with a message saying that we are fixing a previously introduced error.
That is a totally fine approach but we will opt for a different one here. Indeed, we are a bit ashamed and we would like to re-write the history to make it look like we never made this stupid mistake.
How can we use git to do that ?
Luckily for us, this is the best case scenario here since our mistake is in the very last commit and we didn’t actually push our commits on any remote. This means that our local copy is the only place where this commit exists.
We have at least three options here:
Make the modification to the code and use
git commit --amend
to rewrite the latest commit. This is probably the easiest solution but it only works because the error is in the last commit.Use git revert with the hash of the commit we want to undo. This will create a new commit actually undoing the specified commit. We would then modify the file and make a new commit. This is a good approach but our mistake would still be visible in the history of the project.
Use git reset to undo the last commit, make our modifications, and re-make a brand new commit as if nothing ever happened.
Lets’ try the third solution, the first and second ones can be good exercices for the interested reader.
! git reset --hard HEAD~1
HEAD is now at fcd5975 ignore Python generated files
The integer after the ~
symbol specifies how many commits we want to erase. In our case, there is only one.
Warning
We used the --hard
option here which resets both the index AND the working tree. We are doing this to simply re-write our test function but this is VERY dangerous as you could easily lose important work. Usually, it is much better to use the --mixed
option, which is the default anyway, which only resets the index but leaves the tree.
Our project is in a clean state:
! git status
On branch main
nothing to commit, working tree clean
But we lost our latest commit as well as the changes we made to the test file:
! git log -2
commit fcd59755c8f205e887bc502886e5dccacee07932 (HEAD -> main)
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:41:30 2023 +0100
ignore Python generated files
commit d5d3c04553c252334236ef07322675e1f90451ca
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:40:58 2023 +0100
tests: add test function for add
! cat test_myfuncs.py
import pytest
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 42),
(42, 0, 42),
(42, -42, 0),
(-42, 42, 0),
]
)
def test_add(a, b, res):
from myfuncs import add
assert add(a, b) == res
You can clearly see the danger with this command. If we haden’t this notebook with the code we initially wrote, we would have to start our test_sub
function from scratch…
Let’s re-write our test function with the fix:
%%writefile -a test_myfuncs.py
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, -42),
(42, 0, 42),
(42, 42, 0), # Fixed
(42, -42, 84),
(-42, 42, -84),
]
)
def test_sub(a, b, res):
from myfuncs import sub
assert sub(a, b) == res
Appending to test_myfuncs.py
But this time we learn from our mistakes, and we run the test BEFORE committing:
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 11 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 9%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 18%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 27%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 36%]
test_myfuncs.py::test_add[-42-42-0] PASSED [ 45%]
test_myfuncs.py::test_sub[0-0-0] PASSED [ 54%]
test_myfuncs.py::test_sub[0-42--42] PASSED [ 63%]
test_myfuncs.py::test_sub[42-0-42] PASSED [ 72%]
test_myfuncs.py::test_sub[42-42-0] PASSED [ 81%]
test_myfuncs.py::test_sub[42--42-84] PASSED [ 90%]
test_myfuncs.py::test_sub[-42-42--84] PASSED [100%]
============================== 11 passed in 0.02s ==============================
Great! Everything works!
Let’s add and commit these changes:
! git add test_myfuncs.py
! git commit -m "add test function for sub"
[main d94aeda] add test function for sub
1 file changed, 16 insertions(+)
Check the diff contained in this last commit:
! git log -1 -p
commit d94aedaace794abcebfd0dcb9826664df081f380 (HEAD -> main)
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 13:49:57 2023 +0100
add test function for sub
diff --git a/test_myfuncs.py b/test_myfuncs.py
index 97539fb..13824bf 100644
--- a/test_myfuncs.py
+++ b/test_myfuncs.py
@@ -15,3 +15,19 @@ def test_add(a, b, res):
from myfuncs import add
assert add(a, b) == res
+
+@pytest.mark.parametrize(
+ "a,b,res",
+ [
+ (0, 0, 0),
+ (0, 42, -42),
+ (42, 0, 42),
+ (42, 42, 0), # Fixed
+ (42, -42, 84),
+ (-42, 42, -84),
+ ]
+)
+def test_sub(a, b, res):
+ from myfuncs import sub
+
+ assert sub(a, b) == res
Working with remote repositories#
In this section, we are going to add remotes to our project.
First of all, we shouldn’t at this point have any remote configured i our local working copy:
! git remote
Add a remote located somewhere else in the filesystem#
Move to another directory, out of the dummymaths
one, and initialize there a bare repository. We will use it as a remote repository for dummymaths
%cd ..
! mkdir dummymaths_remote
%cd dummymaths_remote
! git init --bare
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths_remote
Initialized empty Git repository in /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths_remote/
Move back to the dummymaths
directory, that contains your initial git working copy and from there add the newly created remote repository. The url of this repository is just a path in your filesystem:
%cd ../dummymaths
! git remote add origin ../dummymaths_remote
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
The project has a new remote called origin
which translates in these two lines:
! git remote -v
origin ../dummymaths_remote (fetch)
origin ../dummymaths_remote (push)
For a same remote, Git makes a difference between fetch/pull and push. Most of the time, these two lines will be identical.
Push your main
branch and enable upstream tracking in the meantime:
! git push origin main -u
Enumerating objects: 15, done.
Counting objects: 100% (15/15), done.
Delta compression using up to 8 threads
Compressing objects: 100% (12/12), done.
Writing objects: 100% (15/15), 1.45 KiB | 744.00 KiB/s, done.
Total 15 (delta 4), reused 0 (delta 0), pack-reused 0
To ../dummymaths_remote
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
Check that the main
branch is now referenced on the remote repository:
! git remote show origin
* remote origin
Fetch URL: ../dummymaths_remote
Push URL: ../dummymaths_remote
HEAD branch: main
Remote branch:
main tracked
Local branch configured for 'git pull':
main merges with remote main
Local ref configured for 'git push':
main pushes to main (up to date)
In another directory, clone a repository hosted on gitlab:
%cd ..
! git clone https://gitlab.inria.fr/git-tutorial/git-tutorial.git
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks
Cloning into 'git-tutorial'...
remote: Enumerating objects: 551, done.
remote: Counting objects: 100% (121/121), done.
remote: Compressing objects: 100% (53/53), done.
remote: Total 551 (delta 55), reused 105 (delta 48), pack-reused 430
Receiving objects: 100% (551/551), 4.88 MiB | 10.53 MiB/s, done.
Resolving deltas: 100% (242/242), done.
You can check the status of your local copy and the information about the remote repository:
%cd git-tutorial
! git status
! git remote -v
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/git-tutorial
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
origin https://gitlab.inria.fr/git-tutorial/git-tutorial.git (fetch)
origin https://gitlab.inria.fr/git-tutorial/git-tutorial.git (push)
Again we have a remote called origin
, but the URL is not a path in our filesystem but the URL of the repository on GitLab.
Manipulating branches#
Move back to the dummymaths
directory and list your local and remote branches:
%cd ../dummymaths
! git branch
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
* main
By default, git branch only lists the local branches. If you want to also list remote branches, you can add the --all
option:
! git branch --all
* main
remotes/origin/main
Create a new branch and work on it#
Create a branch called multiply
and list the branches again:
! git branch multiply
! git branch
* main
multiply
Current branch is still main
but there’s a new multiply
branch listed. Also note how immediate it is to create a new branch.
We can now switch to the multiply
branch with the git switch command, and list the local branches again:
! git switch multiply
! git branch
Switched to branch 'multiply'
main
* multiply
Now let’s display the history of commits on both branches:
! git log --decorate --graph --oneline --all
* d94aeda (HEAD -> multiply, origin/main, main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
Note
You can also try with a graphical tool, such as gitk
using gitk --all
.
Let’s add a new multiply function to the myfuncs.py
module:
%%writefile -a myfuncs.py
def multiply(a, b):
"""Multiply a by b."""
return a * b
Appending to myfuncs.py
Commit the changes above, they should end up in the multiply
branch:
! git commit -am "myfuncs: add the multiply function"
! git log --decorate --graph --oneline --all
[multiply 21d23f3] myfuncs: add the multiply function
1 file changed, 4 insertions(+)
* 21d23f3 (HEAD -> multiply) myfuncs: add the multiply function
* d94aeda (origin/main, main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
The multiply
branch is now one commit ahead of main
.
Now switch back to the main
branch:
! git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
And add a test function to test_myfuncs.py
to test our new multiply function:
%%writefile -a test_myfuncs.py
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
from myfuncs import multiply
assert multiply(a, b) == res
Appending to test_myfuncs.py
Finally, commit the changes above and display the branch history:
! git commit -am "tests: add test function for multiply"
! git log --decorate --graph --oneline --all
[main 3adeb7c] tests: add test function for multiply
1 file changed, 16 insertions(+)
* 3adeb7c (HEAD -> main) tests: add test function for multiply
| * 21d23f3 (multiply) myfuncs: add the multiply function
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
As we can see, the branches main
and multiply
are diverging.
Let’s see how we can merge them in a single branch.
Merging branches#
In this section, we are going to create several branches and merge them in the main
branch.
The fast-forward merge#
Let’s start with a new divide
branch in which we are planning to write a function implementing division:
! git branch divide
! git switch -c todo
Switched to a new branch 'todo'
we can list the different branches of the project:
! git branch
divide
main
multiply
* todo
! git status
On branch todo
nothing to commit, working tree clean
You are now on the todo
branch. Edit the README.md file and add the following content at the end:
%%writefile -a README.md
## TODO
Add _divide_ function
Appending to README.md
! cat README.md
This is the new README file
## TODO
Add _divide_ function
Commit the change above:
! git commit -am "README.md: bootstrap todo section with a divide function item"
! git status
! git log --decorate --graph --oneline --all
[todo d2995f1] README.md: bootstrap todo section with a divide function item
1 file changed, 3 insertions(+)
On branch todo
nothing to commit, working tree clean
* d2995f1 (HEAD -> todo) README.md: bootstrap todo section with a divide function item
* 3adeb7c (main, divide) tests: add test function for multiply
| * 21d23f3 (multiply) myfuncs: add the multiply function
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
The todo
branch is now one commit ahead of the main
branch.
Let’s switch back to main
and merge the todo
branch in main
:
! git switch main
Already on 'main'
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
! git merge todo
Updating 3adeb7c..d2995f1
Fast-forward
README.md | 3 +++
1 file changed, 3 insertions(+)
We are in a very simple case where Git can perform a “fast-forward” merge.
Note
A fast-forward merge can occur when there is a linear path from the current branch tip to the target branch. Instead of “actually” merging the branches, Git only moves (i.e., “fast forward”) the current branch tip up to the target branch tip.
You can read more about fast-forward merge here.
In this case, Git will just “fast-forward” main
to todo
:
! git log --decorate --graph --oneline --all
* d2995f1 (HEAD -> main, todo) README.md: bootstrap todo section with a divide function item
* 3adeb7c (divide) tests: add test function for multiply
| * 21d23f3 (multiply) myfuncs: add the multiply function
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
As good citizens, now that the todo
branch is not needed anymore, let’s remove
it:
! git branch -d todo
! git log --decorate --graph --oneline --all
Deleted branch todo (was d2995f1).
* d2995f1 (HEAD -> main) README.md: bootstrap todo section with a divide function item
* 3adeb7c (divide) tests: add test function for multiply
| * 21d23f3 (multiply) myfuncs: add the multiply function
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
The merge commit case#
The multiply
and main
branches are currently diverging. Normally the changes introduced in multiply
are separate enough from the changes added to main
such that merging multiply
in main
should not conflict.
Merge the multiply
branch into main
:
! git merge multiply --no-edit
Merge made by the 'ort' strategy.
myfuncs.py | 4 ++++
1 file changed, 4 insertions(+)
The merge command created a merge commit:
! git log --decorate --graph --oneline --all
* b1efbf9 (HEAD -> main) Merge branch 'multiply'
|\
| * 21d23f3 (multiply) myfuncs: add the multiply function
* | d2995f1 README.md: bootstrap todo section with a divide function item
* | 3adeb7c (divide) tests: add test function for multiply
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
The conflict!#
Now let’s trigger a conflict on purpose by finally switching to the divide
branch:
! git switch divide
Switched to branch 'divide'
Add the missing divide
function to the myfuncs.py
module:
%%writefile -a myfuncs.py
def divide(a, b):
"""Divide a by b."""
try:
return a / b
except ZeroDivisionError:
return None
Appending to myfuncs.py
And commit that change:
! git commit -am "myfuncs: add divide function"
[divide 05b3427] myfuncs: add divide function
1 file changed, 7 insertions(+)
Add the related test function to the test_myfuncs.py
module:
%%writefile -a test_myfuncs.py
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, None),
(0, 42, 0),
(42, 0, None),
(42, 1, 42),
(1, 2, 0.5),
(-1, 2, -0.5),
]
)
def test_divide(a, b, res):
from myfuncs import divide
assert divide(a, b) == res
Appending to test_myfuncs.py
And commit that change:
! git commit -am "tests: add test function for divide"
[divide 57f967c] tests: add test function for divide
1 file changed, 16 insertions(+)
Now try to merge the divide
branch in the main
branch:
! git switch main
Already on 'main'
Your branch is ahead of 'origin/main' by 4 commits.
(use "git push" to publish your local commits)
! git merge divide --no-edit
Auto-merging myfuncs.py
CONFLICT (content): Merge conflict in myfuncs.py
Automatic merge failed; fix conflicts and then commit the result.
Git is telling us that there is a conflict and that it cannot perform the merge by itself. It requests us to decide how we want to handle each conflict.
In this case, Git is telling us that the problem is located in the file myfuncs
. It is also telling us that we should “fix” the conflict and then commit the result.
Fair enough, but how do we “fix” the conflict ?
Let’s take a look at the problematic file:
! cat myfuncs.py
"""Some useless mathematical utility functions."""
def add(a, b):
"""Return the sum of a and b."""
return a + b
def sub(a, b):
"""Substract b from a."""
return a - b
<<<<<<< HEAD
def multiply(a, b):
"""Multiply a by b."""
return a * b
=======
def divide(a, b):
"""Divide a by b."""
try:
return a / b
except ZeroDivisionError:
return None
>>>>>>> divide
As you can see, Git is showing us the conflicting portions of the code. In our case, the main
branch contains the multiply
function while the divide
branch contains the divide
function.
Solving the conflict in this situation is quite easy for us because we know that we want to keep both functions. However, there is no way for Git to infer that, and this is why we have to step in and decide.
Since we want to keep both functions, we simply need to remove the lines added by Git, save the file, and add it:
! sed -i '' -e 's#<<<<<<< HEAD##' myfuncs.py
! sed -i '' -e 's#>>>>>>> divide##' myfuncs.py
! sed -i '' -e 's#=======##' myfuncs.py
Note
Do not mind the code in the above cell. Usually, you would do that by opening the file in your IDE and manually edit it.
Let’s verify what we just did:
! cat myfuncs.py
"""Some useless mathematical utility functions."""
def add(a, b):
"""Return the sum of a and b."""
return a + b
def sub(a, b):
"""Substract b from a."""
return a - b
def multiply(a, b):
"""Multiply a by b."""
return a * b
def divide(a, b):
"""Divide a by b."""
try:
return a / b
except ZeroDivisionError:
return None
Seems clean enough ! Let’s add it and continue the merge process:
! git add myfuncs.py
! git commit --no-edit
[main 3792095] Merge branch 'divide'
Awesome! The merge was successful and we can verify that with git log
:
! git log --decorate --graph --oneline --all
* 3792095 (HEAD -> main) Merge branch 'divide'
|\
| * 57f967c (divide) tests: add test function for divide
| * 05b3427 myfuncs: add divide function
* | b1efbf9 Merge branch 'multiply'
|\ \
| * | 21d23f3 (multiply) myfuncs: add the multiply function
* | | d2995f1 README.md: bootstrap todo section with a divide function item
| |/
|/|
* | 3adeb7c tests: add test function for multiply
|/
* d94aeda (origin/main) add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
Synchronize your local copy with the remote(s)#
Once all features are merged, it’s time to sync your local main
branch with the remote repository:
! git remote -v
origin ../dummymaths_remote (fetch)
origin ../dummymaths_remote (push)
! git push origin main
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Delta compression using up to 8 threads
Compressing objects: 100% (20/20), done.
Writing objects: 100% (20/20), 2.34 KiB | 1.17 MiB/s, done.
Total 20 (delta 9), reused 0 (delta 0), pack-reused 0
To ../dummymaths_remote
d94aeda..3792095 main -> main
Also, now that the multiply
and divide
branches are not needed anymore, you
can delete them:
! git branch -d multiply divide
Deleted branch multiply (was 21d23f3).
Deleted branch divide (was 57f967c).
Simple rebasing#
In this section, we will learn another way of merging branches without actually performing merge operation, a process called rebasing
.
Let’s again extend the dummymaths
Python project with a power
function.
Check that your current branch is main
(remember that git status is your friend, use it anytime you are unsure of the state of the project):
! git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
Create a new power
branch and switch to it:
! git switch -c power
Switched to a new branch 'power'
Extend the myfuncs.py
module with the power
function :
%%writefile -a myfuncs.py
def power(a, b):
"""Return a power b."""
return a ** b
Appending to myfuncs.py
Commit your change:
! git commit -am "myfuncs: add power function"
[power f70da17] myfuncs: add power function
1 file changed, 4 insertions(+)
Add the related test function to the test_myfuncs.py
module:
%%writefile -a test_myfuncs.py
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 1),
(0, 2, 0),
(1, 2, 1),
(2, 0, 1),
(2, 1, 2),
(2, 2, 4),
(2, -1, 0.5),
]
)
def test_power(a, b, res):
from myfuncs import power
assert power(a, b) == res
Appending to test_myfuncs.py
Commit your change:
! git commit -am "tests: add test function for power"
[power 8ab3cce] tests: add test function for power
1 file changed, 17 insertions(+)
Switch back to main
and remove the TODO
section from the README (the divide function is merged already!).
! git switch main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
! echo "This is the new README file" > README.md
! git add README.md
! git commit -m "README: remove TODO section"
! git log --decorate --graph --oneline --all
[main 548b043] README: remove TODO section
1 file changed, 3 deletions(-)
* 548b043 (HEAD -> main) README: remove TODO section
| * 8ab3cce (power) tests: add test function for power
| * f70da17 myfuncs: add power function
|/
* 3792095 (origin/main) Merge branch 'divide'
|\
| * 57f967c tests: add test function for divide
| * 05b3427 myfuncs: add divide function
* | b1efbf9 Merge branch 'multiply'
|\ \
| * | 21d23f3 myfuncs: add the multiply function
* | | d2995f1 README.md: bootstrap todo section with a divide function item
| |/
|/|
* | 3adeb7c tests: add test function for multiply
|/
* d94aeda add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
At this point, the main
and power
branches have diverged.
We already know how to perform a merge but here we want to perform a rebase. For that, we switch back to the power
branch, and rebase it on top of the main
branch:
! git switch power
Switched to branch 'power'
! git rebase main
Successfully rebased and updated refs/heads/power.
! git log --decorate --graph --oneline --all
* 9be19bc (HEAD -> power) tests: add test function for power
* e07644f myfuncs: add power function
* 548b043 (main) README: remove TODO section
* 3792095 (origin/main) Merge branch 'divide'
|\
| * 57f967c tests: add test function for divide
| * 05b3427 myfuncs: add divide function
* | b1efbf9 Merge branch 'multiply'
|\ \
| * | 21d23f3 myfuncs: add the multiply function
* | | d2995f1 README.md: bootstrap todo section with a divide function item
| |/
|/|
* | 3adeb7c tests: add test function for multiply
|/
* d94aeda add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
Alright! The power
branch is now ahead of main
and the two branches aren’t diverging: this is a case where Git can perform a fast-forward merge!
Let’s do that: merge the power
branch into main
:
! git switch main
Already on 'main'
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
! git merge power --no-edit
Updating 548b043..9be19bc
Fast-forward
myfuncs.py | 4 ++++
test_myfuncs.py | 17 +++++++++++++++++
2 files changed, 21 insertions(+)
! git log --decorate --graph --oneline --all
* 9be19bc (HEAD -> main, power) tests: add test function for power
* e07644f myfuncs: add power function
* 548b043 README: remove TODO section
* 3792095 (origin/main) Merge branch 'divide'
|\
| * 57f967c tests: add test function for divide
| * 05b3427 myfuncs: add divide function
* | b1efbf9 Merge branch 'multiply'
|\ \
| * | 21d23f3 myfuncs: add the multiply function
* | | d2995f1 README.md: bootstrap todo section with a divide function item
| |/
|/|
* | 3adeb7c tests: add test function for multiply
|/
* d94aeda add test function for sub
* fcd5975 ignore Python generated files
* d5d3c04 tests: add test function for add
* d557837 initial version of myfuncs.py
* ceb8dd5 initial commit
Great! The main
branch is now up-to-date with the power
branch!
Finally push main
and delete power
:
! git push origin main
! git branch -d power
Enumerating objects: 13, done.
Counting objects: 100% (13/13), done.
Delta compression using up to 8 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 1.00 KiB | 1.00 MiB/s, done.
Total 9 (delta 4), reused 0 (delta 0), pack-reused 0
To ../dummymaths_remote
3792095..9be19bc main -> main
Deleted branch power (was 9be19bc).
Using bisect to find bugs#
This final section presents a less known, but extremely powerful, Git command: git bisect.
Realize that there is a bug#
For some reason, we decide to run our test suite:
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 30 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 3%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 6%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 10%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 13%]
test_myfuncs.py::test_add[-42-42-0] PASSED [ 16%]
test_myfuncs.py::test_sub[0-0-0] PASSED [ 20%]
test_myfuncs.py::test_sub[0-42--42] PASSED [ 23%]
test_myfuncs.py::test_sub[42-0-42] PASSED [ 26%]
test_myfuncs.py::test_sub[42-42-0] PASSED [ 30%]
test_myfuncs.py::test_sub[42--42-84] PASSED [ 33%]
test_myfuncs.py::test_sub[-42-42--84] PASSED [ 36%]
test_myfuncs.py::test_multiply[0-0-0] PASSED [ 40%]
test_myfuncs.py::test_multiply[0-42-0] PASSED [ 43%]
test_myfuncs.py::test_multiply[42-0-0] PASSED [ 46%]
test_myfuncs.py::test_multiply[42-1-42] PASSED [ 50%]
test_myfuncs.py::test_multiply[1-42-41] FAILED [ 53%]
test_myfuncs.py::test_multiply[-1-42--42] PASSED [ 56%]
test_myfuncs.py::test_divide[0-0-None] PASSED [ 60%]
test_myfuncs.py::test_divide[0-42-0] PASSED [ 63%]
test_myfuncs.py::test_divide[42-0-None] PASSED [ 66%]
test_myfuncs.py::test_divide[42-1-42] PASSED [ 70%]
test_myfuncs.py::test_divide[1-2-0.5] PASSED [ 73%]
test_myfuncs.py::test_divide[-1-2--0.5] PASSED [ 76%]
test_myfuncs.py::test_power[0-0-1] PASSED [ 80%]
test_myfuncs.py::test_power[0-2-0] PASSED [ 83%]
test_myfuncs.py::test_power[1-2-1] PASSED [ 86%]
test_myfuncs.py::test_power[2-0-1] PASSED [ 90%]
test_myfuncs.py::test_power[2-1-2] PASSED [ 93%]
test_myfuncs.py::test_power[2-2-4] PASSED [ 96%]
test_myfuncs.py::test_power[2--1-0.5] PASSED [100%]
=================================== FAILURES ===================================
____________________________ test_multiply[1-42-41] ____________________________
a = 1, b = 42, res = 41
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
from myfuncs import multiply
> assert multiply(a, b) == res
E assert 42 == 41
E + where 42 = <function multiply at 0x7f8d393f6830>(1, 42)
test_myfuncs.py:49: AssertionError
=========================== short test summary info ============================
FAILED test_myfuncs.py::test_multiply[1-42-41] - assert 42 == 41
========================= 1 failed, 29 passed in 0.11s =========================
Looks like our new habit of always running our tests before committing didn’t last long…
We clearly made a mistake somewhere, and even if the problem is obvious in such a simple example, we will use git bisect to find the commit that introduced the problem. In some more complex cases, that can help understand what is the origin of the bug.
Before starting bisect, you must find a commit that works.
We know one: the one that contains the “add function test”. Let’s use git log
with some special parameters to find it:
! git log --oneline --grep "tests: add test function for add"
d5d3c04 tests: add test function for add
The reply contains the short hash of the matching commit that you’ll use as good commit.
Let’s now start bisecting:
! git bisect start
status: waiting for both good and bad commits
We know the current commit is bad because we watched our test suite fail a couple cells before:
! git bisect bad
status: waiting for good commit(s), bad commit known
You now have to manually switch to the commit that we think is good, test it and tell git:
# Change the hash value to the one in the output of the cell above
! git checkout d5d3c04
Note: switching to 'd5d3c04'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d5d3c04 tests: add test function for add
Let’s run our tests. We should have all tests passing:
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 5 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 20%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 40%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 60%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 80%]
test_myfuncs.py::test_add[-42-42-0] PASSED [100%]
============================== 5 passed in 0.01s ===============================
Alright, seems to be the case. Let’s tell Git:
! git bisect good
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[b1efbf9b8e6e98e49215ba9dd8b7de7619e6ced8] Merge branch 'multiply'
Since pytest
is the command that is used to check if a commit is good or
not, you can run it at each step:
! git bisect run pytest
running 'pytest'
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 17 items
test_myfuncs.py ...............F. [100%]
=================================== FAILURES ===================================
____________________________ test_multiply[1-42-41] ____________________________
a = 1, b = 42, res = 41
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
from myfuncs import multiply
> assert multiply(a, b) == res
E assert 42 == 41
E + where 42 = <function multiply at 0x7f8b2933a710>(1, 42)
test_myfuncs.py:49: AssertionError
=========================== short test summary info ============================
FAILED test_myfuncs.py::test_multiply[1-42-41] - assert 42 == 41
========================= 1 failed, 16 passed in 0.09s =========================
Bisecting: 2 revisions left to test after this (roughly 2 steps)
[21d23f3c7c84d2a07253b29b0eb1315f771f3840] myfuncs: add the multiply function
running 'pytest'
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 11 items
test_myfuncs.py ........... [100%]
============================== 11 passed in 0.02s ==============================
Bisecting: 0 revisions left to test after this (roughly 1 step)
[d2995f19b7db82a761182a2df16f4153a13fa91e] README.md: bootstrap todo section with a divide function item
running 'pytest'
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 17 items
test_myfuncs.py ...........FFFFFF [100%]
=================================== FAILURES ===================================
_____________________________ test_multiply[0-0-0] _____________________________
a = 0, b = 0, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[0-42-0] _____________________________
a = 0, b = 42, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[42-0-0] _____________________________
a = 42, b = 0, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[42-1-42] ____________________________
a = 42, b = 1, res = 42
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[1-42-41] ____________________________
a = 1, b = 42, res = 41
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
___________________________ test_multiply[-1-42--42] ___________________________
a = -1, b = 42, res = -42
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
=========================== short test summary info ============================
FAILED test_myfuncs.py::test_multiply[0-0-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[0-42-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[42-0-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[42-1-42] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[1-42-41] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[-1-42--42] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
========================= 6 failed, 11 passed in 0.10s =========================
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[3adeb7cf4da31cce6c2d342520bb8f35e1a78462] tests: add test function for multiply
running 'pytest'
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 17 items
test_myfuncs.py ...........FFFFFF [100%]
=================================== FAILURES ===================================
_____________________________ test_multiply[0-0-0] _____________________________
a = 0, b = 0, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[0-42-0] _____________________________
a = 0, b = 42, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[42-0-0] _____________________________
a = 42, b = 0, res = 0
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[42-1-42] ____________________________
a = 42, b = 1, res = 42
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
____________________________ test_multiply[1-42-41] ____________________________
a = 1, b = 42, res = 41
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
___________________________ test_multiply[-1-42--42] ___________________________
a = -1, b = 42, res = -42
@pytest.mark.parametrize(
"a,b,res",
[
(0, 0, 0),
(0, 42, 0),
(42, 0, 0),
(42, 1, 42),
(1, 42, 41),
(-1, 42, -42),
]
)
def test_multiply(a, b, res):
> from myfuncs import multiply
E ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths/myfuncs.py)
test_myfuncs.py:47: ImportError
=========================== short test summary info ============================
FAILED test_myfuncs.py::test_multiply[0-0-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[0-42-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[42-0-0] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[42-1-42] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[1-42-41] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
FAILED test_myfuncs.py::test_multiply[-1-42--42] - ImportError: cannot import name 'multiply' from 'myfuncs' (/Users/nicolas.g...
========================= 6 failed, 11 passed in 0.10s =========================
3adeb7cf4da31cce6c2d342520bb8f35e1a78462 is the first bad commit
commit 3adeb7cf4da31cce6c2d342520bb8f35e1a78462
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 14:07:23 2023 +0100
tests: add test function for multiply
test_myfuncs.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
bisect found first bad commit
! git bisect reset
Previous HEAD position was 3adeb7c tests: add test function for multiply
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
This command should tell you quite fast what is the first bad commit. Check it’s content:
# Change the hash value to the one in the output of the cell above
! git show 3adeb7c
commit 3adeb7cf4da31cce6c2d342520bb8f35e1a78462
Author: John Doe <john.doe@inria.fr>
Date: Tue Nov 21 14:07:23 2023 +0100
tests: add test function for multiply
diff --git a/test_myfuncs.py b/test_myfuncs.py
index 13824bf..10b2f45 100644
--- a/test_myfuncs.py
+++ b/test_myfuncs.py
@@ -31,3 +31,19 @@ def test_sub(a, b, res):
from myfuncs import sub
assert sub(a, b) == res
+
+@pytest.mark.parametrize(
+ "a,b,res",
+ [
+ (0, 0, 0),
+ (0, 42, 0),
+ (42, 0, 0),
+ (42, 1, 42),
+ (1, 42, 41),
+ (-1, 42, -42),
+ ]
+)
+def test_multiply(a, b, res):
+ from myfuncs import multiply
+
+ assert multiply(a, b) == res
Point 5. reveals that the problem comes from one the multiply test case.
Let’s fix it:
! sed -i '' -e 's#(1, 42, 41),#(1, 42, 42),#' test_myfuncs.py
! pytest -v
============================= test session starts ==============================
platform darwin -- Python 3.10.13, pytest-7.4.3, pluggy-1.3.0 -- /Users/nicolas.gensollen/opt/anaconda3/envs/now/bin/python
cachedir: .pytest_cache
rootdir: /Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks/dummymaths
plugins: anyio-3.5.0, dvc-3.30.1, hydra-core-1.3.2
collected 30 items
test_myfuncs.py::test_add[0-0-0] PASSED [ 3%]
test_myfuncs.py::test_add[0-42-42] PASSED [ 6%]
test_myfuncs.py::test_add[42-0-42] PASSED [ 10%]
test_myfuncs.py::test_add[42--42-0] PASSED [ 13%]
test_myfuncs.py::test_add[-42-42-0] PASSED [ 16%]
test_myfuncs.py::test_sub[0-0-0] PASSED [ 20%]
test_myfuncs.py::test_sub[0-42--42] PASSED [ 23%]
test_myfuncs.py::test_sub[42-0-42] PASSED [ 26%]
test_myfuncs.py::test_sub[42-42-0] PASSED [ 30%]
test_myfuncs.py::test_sub[42--42-84] PASSED [ 33%]
test_myfuncs.py::test_sub[-42-42--84] PASSED [ 36%]
test_myfuncs.py::test_multiply[0-0-0] PASSED [ 40%]
test_myfuncs.py::test_multiply[0-42-0] PASSED [ 43%]
test_myfuncs.py::test_multiply[42-0-0] PASSED [ 46%]
test_myfuncs.py::test_multiply[42-1-42] PASSED [ 50%]
test_myfuncs.py::test_multiply[1-42-42] PASSED [ 53%]
test_myfuncs.py::test_multiply[-1-42--42] PASSED [ 56%]
test_myfuncs.py::test_divide[0-0-None] PASSED [ 60%]
test_myfuncs.py::test_divide[0-42-0] PASSED [ 63%]
test_myfuncs.py::test_divide[42-0-None] PASSED [ 66%]
test_myfuncs.py::test_divide[42-1-42] PASSED [ 70%]
test_myfuncs.py::test_divide[1-2-0.5] PASSED [ 73%]
test_myfuncs.py::test_divide[-1-2--0.5] PASSED [ 76%]
test_myfuncs.py::test_power[0-0-1] PASSED [ 80%]
test_myfuncs.py::test_power[0-2-0] PASSED [ 83%]
test_myfuncs.py::test_power[1-2-1] PASSED [ 86%]
test_myfuncs.py::test_power[2-0-1] PASSED [ 90%]
test_myfuncs.py::test_power[2-1-2] PASSED [ 93%]
test_myfuncs.py::test_power[2-2-4] PASSED [ 96%]
test_myfuncs.py::test_power[2--1-0.5] PASSED [100%]
============================== 30 passed in 0.03s ==============================
Commit your changes and push the main
branch:
! git add test_myfuncs.py
! git commit -am "tests: fix multiply test case"
! git push origin main
[main 0b286b0] tests: fix multiply test case
1 file changed, 1 insertion(+), 1 deletion(-)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 301 bytes | 301.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0
To ../dummymaths_remote
9be19bc..0b286b0 main -> main
Cleaning:
# Cleaning...
%cd ..
! rm -rf dummymaths_remote/ dummymaths/ git-tutorial/
/Users/nicolas.gensollen/GitRepos/NOW-2023/notebooks