Skip to content

Commit 2afeb9a

Browse files
Merge pull request #231 from baloise/feat/pull-rebase
feat: Pull and rebase before push
2 parents 458dca6 + 2b2e98a commit 2afeb9a

File tree

10 files changed

+133
-1
lines changed

10 files changed

+133
-1
lines changed

gitopscli/commands/create_preview.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def __commit_and_push(self, git_repo: GitRepo, message: str) -> None:
101101
self.__args.git_author_email,
102102
message,
103103
)
104+
git_repo.pull_rebase()
104105
git_repo.push()
105106

106107
def __get_gitops_config(self) -> GitOpsConfig:

gitopscli/commands/delete_preview.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def __commit_and_push(self, git_repo: GitRepo, message: str) -> None:
7474
self.__args.git_author_email,
7575
message,
7676
)
77+
git_repo.pull_rebase()
7778
git_repo.push()
7879

7980
@staticmethod

gitopscli/commands/deploy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def execute(self) -> None:
5555
logging.info("All values already up-to-date. I'm done here.")
5656
return
5757

58+
git_repo.pull_rebase()
5859
git_repo.push()
5960

6061
if self.__args.create_pr:

gitopscli/commands/sync_apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@ def __commit_and_push(
103103
git_author_email,
104104
f"{author} updated " + app_file_name,
105105
)
106+
root_config_git_repo.pull_rebase()
106107
root_config_git_repo.push()

gitopscli/git_api/git_repo.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ def __validate_git_author(self, name: str | None, email: str | None) -> None:
105105
if (name and not email) or (not name and email):
106106
raise GitOpsException("Please provide the name and email address of the Git author or provide neither!")
107107

108+
def pull_rebase(self) -> None:
109+
repo = self.__get_repo()
110+
branch = repo.git.branch("--show-current")
111+
if not self.__remote_branch_exists(branch):
112+
return
113+
logging.info("Pull and rebase: %s", branch)
114+
repo.git.pull("--rebase")
115+
108116
def push(self, branch: str | None = None) -> None:
109117
repo = self.__get_repo()
110118
if not branch:
@@ -122,6 +130,10 @@ def get_author_from_last_commit(self) -> str:
122130
last_commit = repo.head.commit
123131
return str(repo.git.show("-s", "--format=%an <%ae>", last_commit.hexsha))
124132

133+
def __remote_branch_exists(self, branch: str) -> bool:
134+
repo = self.__get_repo()
135+
return bool(repo.git.ls_remote("--heads", "origin", f"refs/heads/{branch}").strip() != "")
136+
125137
def __delete_tmp_dir(self) -> None:
126138
if self.__tmp_dir:
127139
delete_tmp_dir(self.__tmp_dir)

tests/commands/test_create_preview.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ def git_repo_api_factory_create_mock(_: GitApiConfig, organisation: str, reposit
107107
self.template_git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/template-repo/{x}"
108108
self.template_git_repo_mock.clone.return_value = None
109109
self.template_git_repo_mock.commit.return_value = None
110+
self.template_git_repo_mock.pull_rebase.return_value = None
110111
self.template_git_repo_mock.push.return_value = None
111112

112113
self.target_git_repo_mock = self.create_mock(GitRepo)
@@ -208,6 +209,7 @@ def test_create_new_preview(self):
208209
"GIT_AUTHOR_EMAIL",
209210
"Create new preview environment for 'my-app' and git hash '3361723dbd91fcfae7b5b8b8b7d462fbc14187a9'.",
210211
),
212+
call.GitRepo.pull_rebase(),
211213
call.GitRepo.push(),
212214
]
213215

@@ -312,6 +314,7 @@ def test_create_new_preview_from_same_template_target_repo(self):
312314
"GIT_AUTHOR_EMAIL",
313315
"Create new preview environment for 'my-app' and git hash '3361723dbd91fcfae7b5b8b8b7d462fbc14187a9'.",
314316
),
317+
call.GitRepo.pull_rebase(),
315318
call.GitRepo.push(),
316319
]
317320

@@ -381,6 +384,7 @@ def test_update_existing_preview(self):
381384
"GIT_AUTHOR_EMAIL",
382385
"Update preview environment for 'my-app' and git hash '3361723dbd91fcfae7b5b8b8b7d462fbc14187a9'.",
383386
),
387+
call.GitRepo.pull_rebase(),
384388
call.GitRepo.push(),
385389
]
386390

tests/commands/test_delete_preview.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def setUp(self):
6363
self.git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}"
6464
self.git_repo_mock.clone.return_value = None
6565
self.git_repo_mock.commit.return_value = None
66+
self.git_repo_mock.pull_rebase.return_value = None
6667
self.git_repo_mock.push.return_value = None
6768

6869
self.seal_mocks()
@@ -100,6 +101,7 @@ def test_delete_existing_happy_flow(self):
100101
"GIT_AUTHOR_EMAIL",
101102
"Delete preview environment for 'APP' and preview id 'PREVIEW_ID'.",
102103
),
104+
call.GitRepo.pull_rebase(),
103105
call.GitRepo.push(),
104106
]
105107

tests/commands/test_deploy.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def setUp(self):
4848
self.git_repo_mock.new_branch.return_value = None
4949
self.example_commit_hash = "5f3a443e7ecb3723c1a71b9744e2993c0b6dfc00"
5050
self.git_repo_mock.commit.return_value = self.example_commit_hash
51+
self.git_repo_mock.pull_rebase.return_value = None
5152
self.git_repo_mock.push.return_value = None
5253
self.git_repo_mock.get_full_file_path.side_effect = lambda x: f"/tmp/created-tmp-dir/{x}"
5354

@@ -101,6 +102,7 @@ def test_happy_flow(self, mock_print):
101102
"GIT_AUTHOR_EMAIL",
102103
"changed 'a.b.d' to 'bar' in test/file.yml",
103104
),
105+
call.GitRepo.pull_rebase(),
104106
call.GitRepo.push(),
105107
]
106108

@@ -142,6 +144,7 @@ def test_create_pr_single_value_change_happy_flow_with_output(self, mock_print):
142144
call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"),
143145
call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"),
144146
call.GitRepo.commit("GIT_USER", "GIT_EMAIL", None, None, "changed 'a.b.c' to 'foo' in test/file.yml"),
147+
call.GitRepo.pull_rebase(),
145148
call.GitRepo.push(),
146149
call.GitRepoApi.create_pull_request_to_default_branch(
147150
"gitopscli-deploy-b973b5bb",
@@ -199,6 +202,7 @@ def test_create_pr_multiple_value_changes_happy_flow_with_output(self, mock_prin
199202
call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"),
200203
call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"),
201204
call.GitRepo.commit("GIT_USER", "GIT_EMAIL", None, None, "changed 'a.b.d' to 'bar' in test/file.yml"),
205+
call.GitRepo.pull_rebase(),
202206
call.GitRepo.push(),
203207
call.GitRepoApi.create_pull_request_to_default_branch(
204208
"gitopscli-deploy-b973b5bb",
@@ -259,6 +263,7 @@ def test_create_pr_and_merge_happy_flow(self, mock_print):
259263
call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"),
260264
call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"),
261265
call.GitRepo.commit("GIT_USER", "GIT_EMAIL", None, None, "changed 'a.b.d' to 'bar' in test/file.yml"),
266+
call.GitRepo.pull_rebase(),
262267
call.GitRepo.push(),
263268
call.GitRepoApi.create_pull_request_to_default_branch(
264269
"gitopscli-deploy-b973b5bb",
@@ -313,6 +318,7 @@ def test_single_commit_happy_flow(self, mock_print):
313318
None,
314319
"updated 2 values in test/file.yml\n\na.b.c: foo\na.b.d: bar",
315320
),
321+
call.GitRepo.pull_rebase(),
316322
call.GitRepo.push(),
317323
]
318324

@@ -352,6 +358,7 @@ def test_single_commit_single_value_change_happy_flow(self, mock_print):
352358
call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.c", "foo"),
353359
call.logging.info("Updated yaml property %s to %s", "a.b.c", "foo"),
354360
call.GitRepo.commit("GIT_USER", "GIT_EMAIL", None, None, "changed 'a.b.c' to 'foo' in test/file.yml"),
361+
call.GitRepo.pull_rebase(),
355362
call.GitRepo.push(),
356363
]
357364

@@ -393,6 +400,7 @@ def test_commit_message_multiple_value_changes_happy_flow(self, mock_print):
393400
call.update_yaml_file("/tmp/created-tmp-dir/test/file.yml", "a.b.d", "bar"),
394401
call.logging.info("Updated yaml property %s to %s", "a.b.d", "bar"),
395402
call.GitRepo.commit("GIT_USER", "GIT_EMAIL", None, None, "testcommit"),
403+
call.GitRepo.pull_rebase(),
396404
call.GitRepo.push(),
397405
]
398406

tests/commands/test_sync_apps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def setUp(self):
7373
self.root_config_git_repo_mock.get_clone_url.return_value = "https://repository.url/root/root-config.git"
7474
self.root_config_git_repo_mock.clone.return_value = None
7575
self.root_config_git_repo_mock.commit.return_value = None
76+
self.root_config_git_repo_mock.pull_rebase.return_value = None
7677
self.root_config_git_repo_mock.push.return_value = None
7778

7879
self.git_repo_api_factory_mock = self.monkey_patch(GitRepoApiFactory)
@@ -166,6 +167,7 @@ def test_sync_apps_happy_flow(self):
166167
"GIT_AUTHOR_EMAIL",
167168
"author updated /tmp/root-config-repo/apps/team-non-prod.yaml",
168169
),
170+
call.GitRepo_root.pull_rebase(),
169171
call.GitRepo_root.push(),
170172
call.GitRepo_root.__exit__(None, None, None),
171173
call.GitRepo_team.__exit__(None, None, None),

tests/git_api/test_git_repo.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def __create_origin(self):
4949
with Path(f"{repo_dir}/README.md").open("w") as readme:
5050
readme.write("xyz branch readme")
5151
repo.git.add("--all")
52-
repo.git.commit("-m", "xyz brach commit", "--author", f"{git_user} <{git_email}>")
52+
repo.git.commit("-m", "initial xyz branch commit", "--author", f"{git_user} <{git_email}>")
5353

5454
repo.git.checkout("master") # master = default branch
5555
repo.git.config("receive.denyCurrentBranch", "ignore")
@@ -313,6 +313,106 @@ def test_commit_nothing_to_commit(self, logging_mock):
313313
self.assertEqual("initial commit\n", commits[0].message)
314314
logging_mock.assert_not_called()
315315

316+
@patch("gitopscli.git_api.git_repo.logging")
317+
def test_pull_rebase_master_single_commit(self, logging_mock):
318+
origin_repo = self.__origin
319+
with GitRepo(self.__mock_repo_api) as testee:
320+
testee.clone()
321+
322+
# local commit
323+
with Path(testee.get_full_file_path("local.md")).open("w") as outfile:
324+
outfile.write("local file")
325+
local_repo = Repo(testee.get_full_file_path("."))
326+
local_repo.git.add("--all")
327+
local_repo.config_writer().set_value("user", "email", "[email protected]").release()
328+
local_repo.git.commit("-m", "local commit")
329+
330+
# origin commit
331+
with Path(f"{origin_repo.working_dir}/origin.md").open("w") as readme:
332+
readme.write("origin file")
333+
origin_repo.git.add("--all")
334+
origin_repo.config_writer().set_value("user", "email", "[email protected]").release()
335+
origin_repo.git.commit("-m", "origin commit")
336+
337+
# pull and rebase from remote
338+
logging_mock.reset_mock()
339+
340+
testee.pull_rebase()
341+
342+
logging_mock.info.assert_called_once_with("Pull and rebase: %s", "master")
343+
344+
# then push should work
345+
testee.push()
346+
347+
commits = list(self.__origin.iter_commits("master"))
348+
self.assertEqual(3, len(commits))
349+
self.assertEqual("initial commit\n", commits[2].message)
350+
self.assertEqual("origin commit\n", commits[1].message)
351+
self.assertEqual("local commit\n", commits[0].message)
352+
353+
@patch("gitopscli.git_api.git_repo.logging")
354+
def test_pull_rebase_remote_branch_single_commit(self, logging_mock):
355+
origin_repo = self.__origin
356+
origin_repo.git.checkout("xyz")
357+
with GitRepo(self.__mock_repo_api) as testee:
358+
testee.clone(branch="xyz")
359+
360+
# local commit
361+
with Path(testee.get_full_file_path("local.md")).open("w") as outfile:
362+
outfile.write("local file")
363+
local_repo = Repo(testee.get_full_file_path("."))
364+
local_repo.git.add("--all")
365+
local_repo.config_writer().set_value("user", "email", "[email protected]").release()
366+
local_repo.git.commit("-m", "local branch commit")
367+
368+
# origin commit
369+
with Path(f"{origin_repo.working_dir}/origin.md").open("w") as readme:
370+
readme.write("origin file")
371+
origin_repo.git.add("--all")
372+
origin_repo.config_writer().set_value("user", "email", "[email protected]").release()
373+
origin_repo.git.commit("-m", "origin branch commit")
374+
375+
# pull and rebase from remote
376+
logging_mock.reset_mock()
377+
378+
testee.pull_rebase()
379+
380+
logging_mock.info.assert_called_once_with("Pull and rebase: %s", "xyz")
381+
382+
# then push should work
383+
testee.push()
384+
385+
commits = list(self.__origin.iter_commits("xyz"))
386+
self.assertEqual(4, len(commits))
387+
self.assertEqual("local branch commit\n", commits[0].message)
388+
self.assertEqual("origin branch commit\n", commits[1].message)
389+
self.assertEqual("initial xyz branch commit\n", commits[2].message)
390+
391+
@patch("gitopscli.git_api.git_repo.logging")
392+
def test_pull_rebase_without_new_commits(self, logging_mock):
393+
with GitRepo(self.__mock_repo_api) as testee:
394+
testee.clone()
395+
396+
# pull and rebase from remote
397+
logging_mock.reset_mock()
398+
399+
testee.pull_rebase()
400+
401+
logging_mock.info.assert_called_once_with("Pull and rebase: %s", "master")
402+
403+
@patch("gitopscli.git_api.git_repo.logging")
404+
def test_pull_rebase_if_no_remote_branch_is_noop(self, logging_mock):
405+
with GitRepo(self.__mock_repo_api) as testee:
406+
testee.clone()
407+
testee.new_branch("new-branch-only-local")
408+
409+
# pull and rebase from remote
410+
logging_mock.reset_mock()
411+
412+
testee.pull_rebase()
413+
414+
logging_mock.assert_not_called()
415+
316416
@patch("gitopscli.git_api.git_repo.logging")
317417
def test_push(self, logging_mock):
318418
with GitRepo(self.__mock_repo_api) as testee:

0 commit comments

Comments
 (0)