Skip to content

Commit baac423

Browse files
Add explicit options for orphan handling in push
If a push introduces new orphan history, there is some ambiguity what that means. It could be a synthetic workspace merge commit, an attempt to import an entirely new subtree or just a new history that has been added to an existing subtree. Previously the "workspace edit" case was assumed and "-p merge" could be used in the "add new subtree" case, leaving no option for the "new history in existing subtree" case. Instead of guessing this change makes the push fail by default and asks the user to be explicit in what to do. Also change `merge_base_many` to `merge_base_octopus` as this is the proper function to check for a common ancestor of multiple commits according to git2 docs.
1 parent 58f633a commit baac423

File tree

9 files changed

+282
-9
lines changed

9 files changed

+282
-9
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

josh-cli/src/bin/josh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ fn handle_push(args: &PushArgs) -> anyhow::Result<()> {
723723
original_target,
724724
old_filtered_oid,
725725
local_commit,
726-
false, // keep_orphans
726+
josh::history::OrphansMode::Keep,
727727
None, // reparent_orphans
728728
&mut changes, // change_ids
729729
)

josh-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ serde_json = { workspace = true }
3535
serde_yaml = { workspace = true }
3636
sled = "0.34.7"
3737
tracing = { workspace = true }
38+
unindent = "0.2.3"

josh-core/src/filter/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ fn apply_to_commit2(
785785
result,
786786
old,
787787
*combine_tip,
788-
false,
788+
history::OrphansMode::Keep,
789789
None,
790790
&mut None,
791791
)?;

josh-core/src/history.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,21 @@ fn find_new_branch_base(
293293
Ok(git2::Oid::zero())
294294
}
295295

296+
#[derive(Clone, Debug)]
297+
pub enum OrphansMode {
298+
Keep,
299+
Remove,
300+
Fail,
301+
}
302+
296303
#[tracing::instrument(skip(transaction, change_ids))]
297304
pub fn unapply_filter(
298305
transaction: &cache::Transaction,
299306
filter: filter::Filter,
300307
original_target: git2::Oid,
301308
old_filtered_oid: git2::Oid,
302309
new_filtered_oid: git2::Oid,
303-
keep_orphans: bool,
310+
orphans_mode: OrphansMode,
304311
reparent_orphans: Option<git2::Oid>,
305312
change_ids: &mut Option<Vec<Change>>,
306313
) -> JoshResult<git2::Oid> {
@@ -382,14 +389,32 @@ pub fn unapply_filter(
382389
}
383390

384391
let mut filtered_parent_ids: Vec<_> = module_commit.parent_ids().collect();
385-
let is_initial_merge = filtered_parent_ids.len() == 2
392+
let has_new_orphan = filtered_parent_ids.len() > 1
386393
&& transaction
387394
.repo()
388-
.merge_base_many(&filtered_parent_ids)
395+
.merge_base_octopus(&filtered_parent_ids)
389396
.is_err();
390397

391-
if !keep_orphans && is_initial_merge {
392-
filtered_parent_ids.pop();
398+
if has_new_orphan {
399+
match orphans_mode {
400+
OrphansMode::Keep => {}
401+
OrphansMode::Remove => {
402+
filtered_parent_ids.pop();
403+
}
404+
OrphansMode::Fail => {
405+
return Err(josh_error(&unindent::unindent(&format!(
406+
r###"
407+
Rejecting new orphan branch at {:?} ({:?})
408+
Specify one of these options:
409+
'-o allow_orphans' to keep the history as is
410+
'-o merge' to import new history by creating merge commit
411+
'-o edit' if you are editing a stored filter or workspace
412+
"###,
413+
module_commit.summary().unwrap_or_default(),
414+
module_commit.id(),
415+
))));
416+
}
417+
}
393418
}
394419

395420
// For every parent of a filtered commit, find unapply base

josh-filter/src/bin/josh-filter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ fn run_filter(args: Vec<String>) -> josh::JoshResult<i32> {
437437
unfiltered_old,
438438
old,
439439
new,
440-
false,
440+
josh::history::OrphansMode::Keep,
441441
None,
442442
&mut None,
443443
) {

josh-proxy/src/lib.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub struct RepoUpdate {
8282
#[derive(Default)]
8383
pub struct PushOptions {
8484
pub merge: bool,
85+
pub allow_orphans: bool,
86+
pub edit: bool,
8587
pub create: bool,
8688
pub force: bool,
8789
pub base: Option<String>,
@@ -230,7 +232,13 @@ pub fn process_repo_update(repo_update: RepoUpdate) -> josh::JoshResult<String>
230232
original_target,
231233
old,
232234
new_oid,
233-
push_options.merge,
235+
if push_options.merge || push_options.allow_orphans {
236+
josh::history::OrphansMode::Keep
237+
} else if push_options.edit {
238+
josh::history::OrphansMode::Remove
239+
} else {
240+
josh::history::OrphansMode::Fail
241+
},
234242
reparent_orphans,
235243
&mut changes,
236244
)?;
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
$ . ${TESTDIR}/setup_test_env.sh
2+
$ cd ${TESTTMP}
3+
4+
$ git clone -q http://localhost:8001/real_repo.git 1> /dev/null
5+
warning: You appear to have cloned an empty repository.
6+
$ cd real_repo
7+
8+
$ mkdir sub1
9+
$ echo contents1 > sub1/file1
10+
$ git add sub1
11+
$ git commit -m "add file1" 1> /dev/null
12+
$ git push 1> /dev/null
13+
To http://localhost:8001/real_repo.git
14+
* [new branch] master -> master
15+
16+
$ cd ${TESTTMP}
17+
18+
$ git clone -q http://localhost:8002/real_repo.git:/sub1.git
19+
$ cd sub1
20+
21+
$ echo contents2 > file2
22+
$ git add file2
23+
$ git commit -m "add file2" 1> /dev/null
24+
$ git checkout --orphan orphan_branch 1> /dev/null
25+
Switched to a new branch 'orphan_branch'
26+
$ echo unrelated > orphan_file
27+
$ git add orphan_file
28+
$ git commit -m "orphan commit" 1> /dev/null
29+
$ git checkout master 1> /dev/null
30+
Switched to branch 'master'
31+
$ git merge --no-ff --allow-unrelated-histories -m "merge orphan" orphan_branch 1> /dev/null
32+
$ git push origin HEAD:refs/heads/new_branch 2>&1 >/dev/null | sed -e 's/[ ]*$//g'
33+
remote: josh-proxy: pre-receive hook
34+
remote: upstream: response status: 500 Internal Server Error
35+
remote: upstream: response body:
36+
remote:
37+
remote: Reference "refs/heads/new_branch" does not exist on remote.
38+
remote: If you want to create it, pass "-o base=<basebranch>" or "-o base=path/to/ref"
39+
remote: to specify a base branch/reference.
40+
remote:
41+
remote: error: hook declined to update refs/heads/new_branch
42+
To http://localhost:8002/real_repo.git:/sub1.git
43+
! [remote rejected] HEAD -> new_branch (hook declined)
44+
error: failed to push some refs to 'http://localhost:8002/real_repo.git:/sub1.git'
45+
46+
$ git push -o base=refs/heads/master -o allow_orphans origin HEAD:refs/heads/new_branch 2>&1 >/dev/null | sed -e 's/[ ]*$//g'
47+
remote: josh-proxy: pre-receive hook
48+
remote: upstream: response status: 200 OK
49+
remote: upstream: response body:
50+
remote:
51+
remote: To http://localhost:8001/real_repo.git
52+
remote: * [new branch] JOSH_PUSH -> new_branch
53+
To http://localhost:8002/real_repo.git:/sub1.git
54+
* [new branch] HEAD -> new_branch
55+
56+
$ curl -s http://localhost:8002/flush
57+
Flushed credential cache
58+
$ git push
59+
remote: josh-proxy: pre-receive hook
60+
remote: upstream: response status: 500 Internal Server Error
61+
remote: upstream: response body:
62+
remote:
63+
remote: Rejecting new orphan branch at "merge orphan" (b960d4fb2014cdabe5caa60b6e3bf8e3f1ee5a05)
64+
remote: Specify one of these options:
65+
remote: '-o allow_orphans' to keep the history as is
66+
remote: '-o merge' to import new history by creating merge commit
67+
remote: '-o edit' if you are editing a stored filter or workspace
68+
remote:
69+
remote: error: hook declined to update refs/heads/master
70+
To http://localhost:8002/real_repo.git:/sub1.git
71+
! [remote rejected] master -> master (hook declined)
72+
error: failed to push some refs to 'http://localhost:8002/real_repo.git:/sub1.git'
73+
[1]
74+
75+
$ git push -o allow_orphans
76+
remote: josh-proxy: pre-receive hook
77+
remote: upstream: response status: 200 OK
78+
remote: upstream: response body:
79+
remote:
80+
remote: To http://localhost:8001/real_repo.git
81+
remote: bb282e9..e61d37d JOSH_PUSH -> master
82+
To http://localhost:8002/real_repo.git:/sub1.git
83+
0b4cf6c..b960d4f master -> master
84+
85+
$ cd ${TESTTMP}/real_repo
86+
$ git pull --rebase
87+
From http://localhost:8001/real_repo
88+
bb282e9..e61d37d master -> origin/master
89+
* [new branch] new_branch -> origin/new_branch
90+
Updating bb282e9..e61d37d
91+
Fast-forward
92+
sub1/file2 | 1 +
93+
sub1/orphan_file | 1 +
94+
2 files changed, 2 insertions(+)
95+
create mode 100644 sub1/file2
96+
create mode 100644 sub1/orphan_file
97+
98+
$ git log --graph --pretty=%s
99+
* merge orphan
100+
|\
101+
| * orphan commit
102+
* add file2
103+
* add file1
104+
105+
$ tree
106+
.
107+
`-- sub1
108+
|-- file1
109+
|-- file2
110+
`-- orphan_file
111+
112+
2 directories, 3 files
113+
114+
$ cat sub1/file2
115+
contents2
116+
117+
Make sure all temporary namespace got removed
118+
$ tree ${TESTTMP}/remote/scratch/real_repo.git/refs/ | grep request_
119+
[1]
120+
121+
$ bash ${TESTDIR}/destroy_test_env.sh
122+
"real_repo.git" = [
123+
":/sub1",
124+
"::sub1/",
125+
]
126+
.
127+
|-- josh
128+
| `-- 24
129+
| `-- sled
130+
| |-- blobs
131+
| |-- conf
132+
| `-- db
133+
|-- mirror
134+
| |-- FETCH_HEAD
135+
| |-- HEAD
136+
| |-- config
137+
| |-- description
138+
| |-- info
139+
| | `-- exclude
140+
| |-- objects
141+
| | |-- 3d
142+
| | | `-- 77ff51363c9825cc2a221fc0ba5a883a1a2c72
143+
| | |-- 6b
144+
| | | `-- 46faacade805991bcaea19382c9d941828ce80
145+
| | |-- 81
146+
| | | `-- b10fb4984d20142cd275b89c91c346e536876a
147+
| | |-- a0
148+
| | | `-- 24003ee1acc6bf70318a46e7b6df651b9dc246
149+
| | |-- a4
150+
| | | `-- ae8248b2e96725156258b90ced9e841dfd20d1
151+
| | |-- b1
152+
| | | `-- d5238086b7f07024d8ed47360e3ce161d9b288
153+
| | |-- ba
154+
| | | `-- 7e17233d9f79c96cb694959eb065302acd96a6
155+
| | |-- bb
156+
| | | `-- 282e9cdc1b972fffd08fd21eead43bc0c83cb8
157+
| | |-- c2
158+
| | | `-- 1c9352f7526e9576892a6631e0e8cf1fccd34d
159+
| | |-- c6
160+
| | | `-- 27a2e3a6bfbb7307f522ad94fdfc8c20b92967
161+
| | |-- c8
162+
| | | `-- 2fc150c43f13cc56c0e9caeba01b58ec612022
163+
| | |-- d8
164+
| | | `-- 43530e8283da7185faac160347db5c70ef4e18
165+
| | |-- e6
166+
| | | `-- 1d37de15923090979cf667263aefa07f78cc33
167+
| | |-- info
168+
| | `-- pack
169+
| `-- refs
170+
| |-- heads
171+
| |-- josh
172+
| | `-- upstream
173+
| | `-- real_repo.git
174+
| | |-- HEAD
175+
| | `-- refs
176+
| | `-- heads
177+
| | |-- master
178+
| | `-- new_branch
179+
| `-- tags
180+
`-- overlay
181+
|-- HEAD
182+
|-- config
183+
|-- description
184+
|-- info
185+
| `-- exclude
186+
|-- objects
187+
| |-- 0b
188+
| | `-- 4cf6c9efbbda1eada39fa9c1d21d2525b027bb
189+
| |-- 4b
190+
| | `-- 825dc642cb6eb9a060e54bf8d69288fbee4904
191+
| |-- 6b
192+
| | `-- 46faacade805991bcaea19382c9d941828ce80
193+
| |-- 81
194+
| | `-- b10fb4984d20142cd275b89c91c346e536876a
195+
| |-- a4
196+
| | `-- ae8248b2e96725156258b90ced9e841dfd20d1
197+
| |-- b1
198+
| | `-- d5238086b7f07024d8ed47360e3ce161d9b288
199+
| |-- b9
200+
| | `-- 60d4fb2014cdabe5caa60b6e3bf8e3f1ee5a05
201+
| |-- ba
202+
| | `-- 7e17233d9f79c96cb694959eb065302acd96a6
203+
| |-- c2
204+
| | `-- 1c9352f7526e9576892a6631e0e8cf1fccd34d
205+
| |-- c6
206+
| | `-- 27a2e3a6bfbb7307f522ad94fdfc8c20b92967
207+
| |-- d8
208+
| | |-- 388f5880393d255b371f1ed9b801d35620017e
209+
| | `-- 43530e8283da7185faac160347db5c70ef4e18
210+
| |-- df
211+
| | `-- b06d7748772bdd407c5911c0ba02b0f5fb31a4
212+
| |-- e6
213+
| | `-- 1d37de15923090979cf667263aefa07f78cc33
214+
| |-- info
215+
| `-- pack
216+
`-- refs
217+
|-- heads
218+
|-- namespaces
219+
`-- tags
220+
221+
53 directories, 41 files
222+
223+
$ cat ${TESTTMP}/josh-proxy.out

tests/proxy/workspace_edit_commit.t

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@
159159

160160
$ git push origin HEAD:refs/for/master 2>&1 >/dev/null | sed -e 's/[ ]*$//g'
161161
remote: josh-proxy: pre-receive hook
162+
remote: upstream: response status: 500 Internal Server Error
163+
remote: upstream: response body:
164+
remote:
165+
remote: Rejecting new orphan branch at "Add new folders" (5645805dcc75cfe4922b9cb301c40a4a4b35a59d)
166+
remote: Specify one of these options:
167+
remote: '-o allow_orphans' to keep the history as is
168+
remote: '-o merge' to import new history by creating merge commit
169+
remote: '-o edit' if you are editing a stored filter or workspace
170+
remote:
171+
remote: error: hook declined to update refs/for/master
172+
To http://localhost:8002/real_repo.git:workspace=ws.git
173+
! [remote rejected] HEAD -> refs/for/master (hook declined)
174+
error: failed to push some refs to 'http://localhost:8002/real_repo.git:workspace=ws.git'
175+
$ git push -o edit origin HEAD:refs/for/master 2>&1 >/dev/null | sed -e 's/[ ]*$//g'
176+
remote: josh-proxy: pre-receive hook
162177
remote: upstream: response status: 200 OK
163178
remote: upstream: response body:
164179
remote:

0 commit comments

Comments
 (0)