From 2dbdfab82d18a86af88d72c7d9993a191430c7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Wed, 17 Sep 2025 16:27:28 +0200 Subject: [PATCH 1/5] Added support to specify '-b branch@commit' Current implementation already allows for specifying explicit commits like '-b commit' to pin subrepo sources (i.e during subrepo clone/pull). In that case, it also sets the name of the branch in the subrepo merge commit message and inside .gitrepo file to that commit hash. This addition allows for specifying both branch and commit. During the subrepo fetch phase, the commit is checked against the specified branch. If the requested commit can be reached from the head of the requested branch, the subrepo is re-fetched to that commit. Afterwards command processing remains the same, except that given branch name is written to the subrepo clone/pull commit message and into the .gitrepo file. The commit part of the '-b branch@commit' request may be abbreviated to the reasonable extent. --- lib/git-subrepo | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/git-subrepo b/lib/git-subrepo index 954380b8..eb2520c1 100755 --- a/lib/git-subrepo +++ b/lib/git-subrepo @@ -118,6 +118,7 @@ main() { local subrepo_remote= # Remote url for subrepo's upstream repo local subrepo_branch= # Upstream branch to clone/push/pull + local subrepo_branch_commit= # Upstream branch commit to clone local subrepo_commit= # Upstream HEAD from previous clone/pull local subrepo_parent= # Local commit from before previous clone/pull local subrepo_former= # A retired gitrepo key that might still exist @@ -727,6 +728,13 @@ subrepo:fetch() { RUN git fetch --no-tags --quiet "$subrepo_remote" "$subrepo_branch" OK || return + if [[ -n "$subrepo_branch_commit" ]]; then + reset-upstream-subrepo-branch-commit + o "Re-fetch the upstream: $subrepo_remote ($subrepo_branch@$subrepo_branch_commit)." + RUN git fetch --no-tags --quiet "$subrepo_remote" "$subrepo_branch_commit" + OK || return + fi + o "Get the upstream subrepo HEAD commit." OUT=true RUN git rev-parse FETCH_HEAD^0 upstream_head_commit=$output @@ -1093,8 +1101,11 @@ get-command-options() { -a) all_wanted=true ;; -A) ALL_wanted=true all_wanted=true ;; - -b) subrepo_branch=$1 - override_branch=$1 + -b) subrepo_branch=${1%%@*} + override_branch=${1%%@*} + if [ ${subrepo_branch} != "${1}" ]; then + subrepo_branch_commit=${1#*@} + fi commit_msg_args+=("--branch=$1") shift ;; -e) edit_wanted=true ;; @@ -1650,6 +1661,25 @@ add-subrepo() { subrepos+=("$1") } +# Determine and set real commit behind branch@commit request +reset-upstream-subrepo-branch-commit() { + local branch_commit + OUT=true RUN git rev-parse FETCH_HEAD^0 + branch_commit=$output + [[ -n "$branch_commit" ]] || + error "Problem finding head commit in upstream branch '${subrepo_branch}'." + + if [[ ! "${branch_commit}" =~ ^$subrepo_branch_commit ]]; then + OUT=true FAIL=false RUN git rev-list --parents $subrepo_branch_commit..$branch_commit + branch_commit=$( + echo "$output" | sed -e '$!d' -e 's/.* //' + ) + [[ -n "$branch_commit" ]] || + error "Problem finding '${subrepo_branch_commit}' commit in upstream branch '${subrepo_branch}'." + fi + subrepo_branch_commit=$branch_commit +} + # Determine the upstream's default head branch: get-upstream-head-branch() { local remotes branch From 9e33d478e46954bf87e6357089c456cda4673c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Sun, 21 Sep 2025 19:16:34 +0200 Subject: [PATCH 2/5] Extend output in subrepo clone, fetch and pull When branch@commit is requested in clone, fetch and pull commands, show that in their output as well. --- lib/git-subrepo | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/git-subrepo b/lib/git-subrepo index eb2520c1..83bcbb96 100755 --- a/lib/git-subrepo +++ b/lib/git-subrepo @@ -206,7 +206,11 @@ command:clone() { local re= $force_wanted && re=re local remote=$subrepo_remote - say "Subrepo '$remote' ($subrepo_branch) ${re}cloned into '$subdir'." + local bc=$subrepo_branch + if [ -n "$subrepo_branch_commit" ]; then + bc=$subrepo_branch@$subrepo_branch_commit + fi + say "Subrepo '$remote' ($bc) ${re}cloned into '$subdir'." } # `git subrepo init ` command: @@ -237,7 +241,11 @@ command:pull() { subrepo:pull if OK; then - say "Subrepo '$subdir' pulled from '$subrepo_remote' ($subrepo_branch)." + local bc=$subrepo_branch + if [ -n "$subrepo_branch_commit" ]; then + bc=$subrepo_branch@$subrepo_branch_commit + fi + say "Subrepo '$subdir' pulled from '$subrepo_remote' ($bc)." elif [[ $CODE -eq -1 ]]; then say "Subrepo '$subdir' is up to date." elif [[ $CODE -eq 1 ]]; then @@ -275,7 +283,11 @@ command:fetch() { say "Ignored '$subdir', no remote." else subrepo:fetch - say "Fetched '$subdir' from '$subrepo_remote' ($subrepo_branch)." + local bc=$subrepo_branch + if [ -n "$subrepo_branch_commit" ]; then + bc=$subrepo_branch@$subrepo_branch_commit + fi + say "Fetched '$subdir' from '$subrepo_remote' ($bc)." fi } From dfbe16f2b9535e80eb3aef800da447b19c036fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Sun, 21 Sep 2025 19:21:57 +0200 Subject: [PATCH 3/5] Added test scenario for using branch@commit It does the following: 1. Clone default test repos foo and bar 2. Create a new branch bar1 in bar 3. In bar create three additional commits in bar1 4. Checkout default branch in bar (to allow for later push) 5. In foo subrepo clone of bar1@commit1 from bar 6. Test clone results 7. In foo subrepo pull of bar1@commit2 from bar 8. Test pull results 9. Make additional change in foo/bar 10. In foo subrepo push bar and test result (push fails) 11. In foo force subrepo clone bar@commit3 (last commit in bar1) 12. Re-make additional change in foo/bar 13. In foo subrepo push bar and test result (push success) 13. In foo subrepo pull bar1 from bar and test result (up to date) 14. In foo subrepo pull bar1@commit2 (fails - old commit) 15. In foo subrepo clone bar1@commit2 (fails - subrepo exists) --- test/use-branch-at-commit.t | 368 ++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 test/use-branch-at-commit.t diff --git a/test/use-branch-at-commit.t b/test/use-branch-at-commit.t new file mode 100644 index 00000000..0046c554 --- /dev/null +++ b/test/use-branch-at-commit.t @@ -0,0 +1,368 @@ +#!/usr/bin/env bash + +set -e + +source test/setup + +use Test::More + +clone-foo-and-bar + +# Test that the repos look ok: +{ + test-exists \ + "$OWNER/foo/.git/" \ + "$OWNER/foo/Foo" \ + "!$OWNER/foo/bar/" \ + "$OWNER/bar/.git/" \ + "$OWNER/bar/Bar" +} + +# Make a new branch bar1 in bar +( + cd $OWNER/bar + git checkout -b bar1 +) &> /dev/null || die + +# check the new bar1 branch: +{ + is "$( + cd "$OWNER/bar" + git branch | grep "^*" + )" \ + "* bar1" \ + 'bar: The repo branch is correct' +} + +# Make a few new commits in a new branch bar1 +( + cd $OWNER/bar + echo "Added first change to Bar" >> Bar + git add Bar + git commit -m"Added first commit to Bar" +) &> /dev/null || die + +{ + cd $OWNER/bar + bar_commit1=$(git rev-parse HEAD) + is "$( + cd "$OWNER/bar" + git log -1 --oneline | sed -e 's/ .*//g' + )" \ + "${bar_commit1:0:7}" \ + 'bar: The first added commit is correct' +} + +( + cd $OWNER/bar + echo "Added second change to Bar" >> Bar + git add Bar + git commit -m"Added second commit to Bar" +) &> /dev/null || die + +{ + cd $OWNER/bar + bar_commit2=$(git rev-parse HEAD) + is "$( + cd "$OWNER/bar" + git log -1 --oneline | sed -e 's/ .*//g' + )" \ + "${bar_commit2:0:7}" \ + 'bar: The second added commit is correct' +} + +( + cd $OWNER/bar + echo "Added third change to Bar" >> Bar + git add Bar + git commit -m"Added third commit to Bar" +) &> /dev/null || die + +{ + cd $OWNER/bar + bar_commit3=$(git rev-parse HEAD) + is "$( + cd "$OWNER/bar" + git log -1 --oneline | sed -e 's/ .*//g' + )" \ + "${bar_commit3:0:7}" \ + 'bar: The third added commit is correct' +} + +# Checkout default branch in bar +( + cd $OWNER/bar + git checkout ${DEFAULTBRANCH} +) &> /dev/null || die + +# check the default branch: +{ + is "$( + cd "$OWNER/bar" + git branch | grep "^*" + )" \ + "* ${DEFAULTBRANCH}" \ + 'bar: The repo branch is correct' +} + +# Do the subrepo clone branch@commit1 and test the output: +{ + clone_output=$( + cd "$OWNER/foo" + git subrepo clone -b bar1@${bar_commit1:0:8} "$OWNER/bar" + ) + + # Check output is correct: + is "$clone_output" \ + "Subrepo '$OWNER/bar' (bar1@${bar_commit1}) cloned into 'bar'." \ + 'subrepo clone command output is correct' + + is "$( + cd "$OWNER/foo" + git remote -v | grep subrepo/bar + )" \ + "" \ + 'No remotes created' +} + +# Check that subrepo files look ok: +gitrepo=$OWNER/foo/bar/.gitrepo +{ + test-exists \ + "$OWNER/foo/bar/" \ + "$OWNER/foo/bar/Bar" \ + "$gitrepo" +} + +# Test foo/bar/.gitrepo file contents: +{ + foo_clone_commit=$(cd "$OWNER/foo"; git rev-parse HEAD^) + test-gitrepo-comment-block + test-gitrepo-field "remote" "$OWNER/bar" + test-gitrepo-field "branch" "bar1" + test-gitrepo-field "commit" "$bar_commit1" + test-gitrepo-field "parent" "$foo_clone_commit" + test-gitrepo-field "cmdver" "$(git subrepo --version)" +} + +# Make sure status is clean: +{ + git_status=$( + cd "$OWNER/foo" + git status -s + ) + + is "$git_status" \ + "" \ + 'status is clean' +} + +# Do the subrepo pull branch@commit2 and test the output: +{ + pull_output=$( + cd "$OWNER/foo" + git subrepo pull -b bar1@${bar_commit2:0:8} bar + ) + + # Check output is correct: + is "$pull_output" \ + "Subrepo 'bar' pulled from '$OWNER/bar' (bar1@${bar_commit2})." \ + 'subrepo pull command output is correct' + + is "$( + cd "$OWNER/foo" + git remote -v | grep subrepo/bar + )" \ + "" \ + 'No remotes created' +} + +# Check that subrepo files look ok: +gitrepo=$OWNER/foo/bar/.gitrepo +{ + test-exists \ + "$OWNER/foo/bar/" \ + "$OWNER/foo/bar/Bar" \ + "$gitrepo" +} + +# Test foo/bar/.gitrepo file contents: +{ + foo_clone_commit=$(cd "$OWNER/foo"; git rev-parse HEAD^) + test-gitrepo-comment-block + test-gitrepo-field "remote" "$OWNER/bar" + test-gitrepo-field "branch" "bar1" + test-gitrepo-field "commit" "$bar_commit2" + test-gitrepo-field "parent" "$foo_clone_commit" + test-gitrepo-field "cmdver" "$(git subrepo --version)" +} + +# Make sure status is clean: +{ + git_status=$( + cd "$OWNER/foo" + git status -s + ) + + is "$git_status" \ + "" \ + 'status is clean' +} + +# Make additional change in foo/bar +( + cd $OWNER/foo + echo "Added first change to foo/bar/Bar" >> bar/Bar + git add bar/Bar + git commit -m"Added first commit to foo/bar/Bar" + #foo_commit1=$(git rev-parse HEAD) +) &> /dev/null || die + +{ + cd $OWNER/foo + foo_commit1=$(git rev-parse HEAD) + is "$( + cd "$OWNER/foo" + git log -1 --oneline | sed -e 's/ .*//g' + )" \ + "${foo_commit1:0:7}" \ + 'foo: The first added commit is correct' +} + +# Do the subrepo push and test output +{ + message=$( + cd "$OWNER/foo" + git subrepo push bar 2>&1 || true + ) + + is "$message" "git-subrepo: There are new changes upstream, you need to pull first." \ + 'Subrepo push command output is correct' +} + +# Do the subrepo forced clone branch@commit3and test the output: +{ + clone_output=$( + cd "$OWNER/foo" + git subrepo clone -f -b bar1@${bar_commit3:0:8} "$OWNER/bar" + ) + + # Check output is correct: + is "$clone_output" \ + "Subrepo '$OWNER/bar' (bar1@${bar_commit3}) recloned into 'bar'." \ + 'subrepo clone command output is correct' + + is "$( + cd "$OWNER/foo" + git remote -v | grep subrepo/bar + )" \ + "" \ + 'No remotes created' +} + +# Check that subrepo files look ok: +gitrepo=$OWNER/foo/bar/.gitrepo +{ + test-exists \ + "$OWNER/foo/bar/" \ + "$OWNER/foo/bar/Bar" \ + "$gitrepo" +} + +# Test foo/bar/.gitrepo file contents: +{ + foo_clone_commit=$(cd "$OWNER/foo"; git rev-parse HEAD^) + test-gitrepo-comment-block + test-gitrepo-field "remote" "$OWNER/bar" + test-gitrepo-field "branch" "bar1" + test-gitrepo-field "commit" "$bar_commit3" + test-gitrepo-field "parent" "$foo_clone_commit" + test-gitrepo-field "cmdver" "$(git subrepo --version)" +} + +# Make sure status is clean: +{ + git_status=$( + cd "$OWNER/foo" + git status -s + ) + + is "$git_status" \ + "" \ + 'status is clean' +} + +# Make additional change in foo/bar +( + cd $OWNER/foo + echo "Added first change to foo/bar/Bar" >> bar/Bar + git add bar/Bar + git commit -m"Added first commit to foo/bar/Bar" + #foo_commit1=$(git rev-parse HEAD) +) &> /dev/null || die + +{ + cd $OWNER/foo + foo_commit1=$(git rev-parse HEAD) + is "$( + cd "$OWNER/foo" + git log -1 --oneline | sed -e 's/ .*//g' + )" \ + "${foo_commit1:0:7}" \ + 'foo: The first added commit is correct' +} + +# Do the subrepo push and test output +{ + message=$( + cd "$OWNER/foo" + git subrepo push bar 2>&1 || true + ) + + is "$message" \ + "Subrepo 'bar' pushed to '$OWNER/bar' (bar1)." \ + 'Subrepo push command output is correct' +} + +# Do the subrepo pull branch and test the output: +{ + pull_output=$( + cd "$OWNER/foo" + git subrepo pull -b bar1 bar + ) + + # Check output is correct: + is "$pull_output" \ + "Subrepo 'bar' is up to date." \ + 'subrepo pull command output is correct' +} + +# Do the subrepo pull branch@commit2 and test the output: +{ + pull_output=$( + cd "$OWNER/foo" + git subrepo pull -b bar1@${bar_commit2:0:8} bar 2>&1 | sed -e 's/not contain.*/not contain/' || true + ) + + # Check output is correct: + is "$pull_output" \ + "git-subrepo: Local repository does not contain" \ + 'subrepo pull command output is correct' +} + +# Do the subrepo clone branch@commit2 and test the output: +{ + clone_output=$( + cd "$OWNER/foo" + git subrepo clone -b bar1@${bar_commit2:0:8} "$OWNER/bar" 2>&1 || true + ) + + # Check output is correct: + is "$clone_output" \ + "git-subrepo: The subdir 'bar' exists and is not empty." \ + 'subrepo clone command output is correct' +} + +done_testing + +teardown From a028a42badf0c3e23c931e92ab0150b757fe3fba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Tue, 21 Oct 2025 14:36:05 +0200 Subject: [PATCH 4/5] Fixed shellcheck errors --- lib/git-subrepo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git-subrepo b/lib/git-subrepo index 83bcbb96..4aed1897 100755 --- a/lib/git-subrepo +++ b/lib/git-subrepo @@ -1115,7 +1115,7 @@ get-command-options() { all_wanted=true ;; -b) subrepo_branch=${1%%@*} override_branch=${1%%@*} - if [ ${subrepo_branch} != "${1}" ]; then + if [ "${subrepo_branch}" != "${1}" ]; then subrepo_branch_commit=${1#*@} fi commit_msg_args+=("--branch=$1") @@ -1682,7 +1682,7 @@ reset-upstream-subrepo-branch-commit() { error "Problem finding head commit in upstream branch '${subrepo_branch}'." if [[ ! "${branch_commit}" =~ ^$subrepo_branch_commit ]]; then - OUT=true FAIL=false RUN git rev-list --parents $subrepo_branch_commit..$branch_commit + OUT=true FAIL=false RUN git rev-list --parents "$subrepo_branch_commit".."$branch_commit" branch_commit=$( echo "$output" | sed -e '$!d' -e 's/.* //' ) From eeb027f6b9ce199d6abee6c02007c71656647a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samo=20Poga=C4=8Dnik?= Date: Tue, 21 Oct 2025 14:40:10 +0200 Subject: [PATCH 5/5] Fixed trailing whitespaces --- test/use-branch-at-commit.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/use-branch-at-commit.t b/test/use-branch-at-commit.t index 0046c554..b918bdae 100644 --- a/test/use-branch-at-commit.t +++ b/test/use-branch-at-commit.t @@ -18,7 +18,7 @@ clone-foo-and-bar "$OWNER/bar/Bar" } -# Make a new branch bar1 in bar +# Make a new branch bar1 in bar ( cd $OWNER/bar git checkout -b bar1 @@ -89,7 +89,7 @@ clone-foo-and-bar 'bar: The third added commit is correct' } -# Checkout default branch in bar +# Checkout default branch in bar ( cd $OWNER/bar git checkout ${DEFAULTBRANCH}