Skip to content

Commit 5062115

Browse files
committed
Add branches to push subcommand
1 parent c8109c6 commit 5062115

File tree

6 files changed

+167
-16
lines changed

6 files changed

+167
-16
lines changed

src/subcommand/push_subcommand.cpp

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
#include <iostream>
44

55
#include <git2/remote.h>
6+
#include <git2/types.h>
67

8+
#include "../utils/ansi_code.hpp"
79
#include "../utils/credentials.hpp"
810
#include "../utils/progress.hpp"
911
#include "../wrapper/repository_wrapper.hpp"
@@ -13,8 +15,15 @@ push_subcommand::push_subcommand(const libgit2_object&, CLI::App& app)
1315
auto* sub = app.add_subcommand("push", "Update remote refs along with associated objects");
1416

1517
sub->add_option("<remote>", m_remote_name, "The remote to push to")->default_val("origin");
16-
18+
sub->add_option("<branch>", m_branch_name, "The branch to push");
1719
sub->add_option("<refspec>", m_refspecs, "The refspec(s) to push");
20+
sub->add_flag(
21+
"--all,--branches",
22+
m_branches_flag,
23+
"Push all branches (i.e. refs under " + ansi_code::bold + "refs/heads/" + ansi_code::reset
24+
+ "); cannot be used with other <refspec>."
25+
);
26+
1827

1928
sub->callback(
2029
[this]()
@@ -37,25 +46,60 @@ void push_subcommand::run()
3746
push_opts.callbacks.push_transfer_progress = push_transfer_progress;
3847
push_opts.callbacks.push_update_reference = push_update_reference;
3948

40-
if (m_refspecs.empty())
49+
if (m_branches_flag)
4150
{
42-
try
51+
auto iter = repo.iterate_branches(GIT_BRANCH_LOCAL);
52+
auto br = iter.next();
53+
while (br)
4354
{
44-
auto head_ref = repo.head();
45-
std::string short_name = head_ref.short_name();
46-
std::string refspec = "refs/heads/" + short_name;
55+
std::string refspec = "refs/heads/" + std::string(br->name());
4756
m_refspecs.push_back(refspec);
57+
br = iter.next();
58+
}
59+
}
60+
else if (m_refspecs.empty())
61+
{
62+
std::string branch;
63+
if (!m_branch_name.empty())
64+
{
65+
branch = m_branch_name;
4866
}
49-
catch (...)
67+
else
5068
{
51-
std::cerr << "Could not determine current branch to push." << std::endl;
52-
return;
69+
try
70+
{
71+
auto head_ref = repo.head();
72+
branch = head_ref.short_name();
73+
}
74+
catch (...)
75+
{
76+
std::cerr << "Could not determine current branch to push." << std::endl;
77+
return;
78+
}
5379
}
80+
std::string refspec = "refs/heads/" + branch;
81+
m_refspecs.push_back(refspec);
5482
}
5583
git_strarray_wrapper refspecs_wrapper(m_refspecs);
5684
git_strarray* refspecs_ptr = nullptr;
5785
refspecs_ptr = refspecs_wrapper;
5886

5987
remote.push(refspecs_ptr, &push_opts);
60-
std::cout << "Pushed to " << remote_name << std::endl;
88+
89+
std::cout << "To " << remote.url() << std::endl;
90+
for (const auto& refspec : m_refspecs)
91+
{
92+
std::string_view ref_view(refspec);
93+
std::string_view prefix = "refs/heads/";
94+
std::string short_name;
95+
if (ref_view.substr(0, prefix.size()) == prefix)
96+
{
97+
short_name = ref_view.substr(prefix.size());
98+
}
99+
else
100+
{
101+
short_name = refspec;
102+
}
103+
std::cout << " * " << short_name << " -> " << short_name << std::endl;
104+
}
61105
}

src/subcommand/push_subcommand.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ class push_subcommand
1717
private:
1818

1919
std::string m_remote_name;
20+
std::string m_branch_name;
2021
std::vector<std::string> m_refspecs;
22+
bool m_branches_flag = false;
2123
};

src/utils/ansi_code.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ namespace ansi_code
1919
const std::string hide_cursor = "\e[?25l";
2020
const std::string show_cursor = "\e[?25h";
2121

22+
const std::string bold = "\033[1m";
23+
const std::string reset = "\033[0m";
24+
2225
// Functions.
2326
std::string cursor_to_row(size_t row);
2427

src/utils/progress.cpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,9 @@ int push_update_reference(const char* refname, const char* status, void*)
139139
{
140140
if (status)
141141
{
142-
std::cout << " " << refname << " " << status << std::endl;
143-
}
144-
else
145-
{
146-
std::cout << " " << refname << std::endl;
142+
std::cout << " ! [remote rejected] " << refname << " (" << status << ")" << std::endl;
143+
return -1;
147144
}
145+
148146
return 0;
149147
}

src/utils/progress.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
#pragma once
22

3+
#include <string>
4+
35
#include <git2.h>
46

57
int sideband_progress(const char* str, int len, void*);
68
int fetch_progress(const git_indexer_progress* stats, void* payload);
79
void checkout_progress(const char* path, size_t cur, size_t tot, void* payload);
810
int update_refs(const char* refname, const git_oid* a, const git_oid* b, git_refspec*, void*);
911
int push_transfer_progress(unsigned int current, unsigned int total, size_t bytes, void*);
12+
13+
struct push_update_payload
14+
{
15+
std::string url;
16+
bool header_printed = false;
17+
};
18+
1019
int push_update_reference(const char* refname, const char* status, void*);

test/test_push.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,99 @@ def test_push_private_repo(
6161
assert p_push.returncode == 0
6262
assert p_push.stdout.count("Username:") == 2
6363
assert p_push.stdout.count("Password:") == 2
64-
assert "Pushed to origin" in p_push.stdout
64+
assert " * [new branch] test-" in p_push.stdout
65+
66+
67+
def test_push_branch_private_repo(
68+
git2cpp_path, tmp_path, run_in_tmp_path, private_test_repo, commit_env_config
69+
):
70+
"""Test push with an explicit branch name: git2cpp push <remote> <branch>."""
71+
branch_name = f"test-{uuid4()}"
72+
73+
username = "abc"
74+
password = private_test_repo["token"]
75+
input = f"{username}\n{password}"
76+
repo_path = tmp_path / private_test_repo["repo_name"]
77+
url = private_test_repo["https_url"]
78+
79+
# Clone the private repo.
80+
clone_cmd = [git2cpp_path, "clone", url]
81+
p_clone = subprocess.run(clone_cmd, capture_output=True, text=True, input=input)
82+
assert p_clone.returncode == 0
83+
assert repo_path.exists()
84+
85+
# Create a new branch and commit on it.
86+
checkout_cmd = [git2cpp_path, "checkout", "-b", branch_name]
87+
p_checkout = subprocess.run(checkout_cmd, cwd=repo_path, capture_output=True, text=True)
88+
assert p_checkout.returncode == 0
89+
90+
(repo_path / "push_branch_file.txt").write_text("push branch test")
91+
subprocess.run([git2cpp_path, "add", "push_branch_file.txt"], cwd=repo_path, check=True)
92+
subprocess.run([git2cpp_path, "commit", "-m", "branch commit"], cwd=repo_path, check=True)
93+
94+
# Switch back to main so HEAD is NOT on the branch we want to push.
95+
subprocess.run(
96+
[git2cpp_path, "checkout", "main"], capture_output=True, check=True, cwd=repo_path
97+
)
98+
99+
status_cmd = [git2cpp_path, "status"]
100+
p_status = subprocess.run(status_cmd, cwd=repo_path, capture_output=True, text=True)
101+
assert p_status.returncode == 0
102+
assert "On branch main" in p_status.stdout
103+
104+
# Push specifying the branch explicitly (HEAD is on main, not the test branch).
105+
input = f"{username}\n{password}"
106+
push_cmd = [git2cpp_path, "push", "origin", branch_name]
107+
p_push = subprocess.run(push_cmd, cwd=repo_path, capture_output=True, text=True, input=input)
108+
assert p_push.returncode == 0
109+
# assert " * [new branch] test-" in p_push.stdout
110+
print("\n\n", p_push.stdout)
111+
112+
113+
def test_push_branches_flag_private_repo(
114+
git2cpp_path, tmp_path, run_in_tmp_path, private_test_repo, commit_env_config
115+
):
116+
"""Test push --branches pushes all local branches."""
117+
branch_a = f"test-a-{uuid4()}"
118+
branch_b = f"test-b-{uuid4()}"
119+
120+
username = "abc"
121+
password = private_test_repo["token"]
122+
input = f"{username}\n{password}"
123+
repo_path = tmp_path / private_test_repo["repo_name"]
124+
url = private_test_repo["https_url"]
125+
126+
# Clone the private repo.
127+
clone_cmd = [git2cpp_path, "clone", url]
128+
p_clone = subprocess.run(clone_cmd, capture_output=True, text=True, input=input)
129+
assert p_clone.returncode == 0
130+
assert repo_path.exists()
131+
132+
# Create two extra branches with commits.
133+
for branch_name in [branch_a, branch_b]:
134+
subprocess.run(
135+
[git2cpp_path, "checkout", "-b", branch_name],
136+
capture_output=True,
137+
check=True,
138+
cwd=repo_path,
139+
)
140+
(repo_path / f"{branch_name}.txt").write_text(f"content for {branch_name}")
141+
subprocess.run([git2cpp_path, "add", f"{branch_name}.txt"], cwd=repo_path, check=True)
142+
subprocess.run(
143+
[git2cpp_path, "commit", "-m", f"commit on {branch_name}"],
144+
cwd=repo_path,
145+
check=True,
146+
)
147+
148+
# Go back to main.
149+
subprocess.run(
150+
[git2cpp_path, "checkout", "main"], capture_output=True, check=True, cwd=repo_path
151+
)
152+
153+
# Push all branches at once.
154+
input = f"{username}\n{password}"
155+
push_cmd = [git2cpp_path, "push", "origin", "--branches"]
156+
p_push = subprocess.run(push_cmd, cwd=repo_path, capture_output=True, text=True, input=input)
157+
assert p_push.returncode == 0
158+
# assert " * [new branch] test-" in p_push.stdout
159+
print("\n\n", p_push.stdout)

0 commit comments

Comments
 (0)