Skip to content

Commit 4353753

Browse files
committed
umpf: add built-in support for synchronizing topic branches
While umpf was explicitly developed with multiple developers adding utags and sharing topic branches in mind, it is less than ideal when there are multiple developers working on the same topic branches. And it frequently leads to one of two issues: - The branch is pushed before the utag is accepted into the BSP repository and other developers get an unexpected addition to their umpf - The branch is not pushed after the utag is accepted into the BSP repository and other developers get an unexpected removal from their umpf Every time this happens, it wastes a bit of time to identify what went wrong and thus a solution built into umpf is appropriate: - CI will call umpf --remote=downstream --force push $BSP/series.inc when a PR touching a useries is accepted - Developers can call umpf pull to synchronize their topic branches or to find out when difference they have to the now upstream version Signed-off-by: Ahmad Fatoum <[email protected]>
1 parent 10a5951 commit 4353753

File tree

3 files changed

+244
-2
lines changed

3 files changed

+244
-2
lines changed

bash_completion

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ _umpf_completion()
3838
"")
3939
COMPREPLY=( $( compgen -W "${completion_cmds[*]} help" -- $cur ) )
4040
;;
41-
diff|show|tag|tig|build)
41+
diff|show|tag|tig|build|push|pull)
4242
local -a refs
4343
refs=( $( compgen -W "$( git for-each-ref --format='%(refname:short)' refs/tags refs/heads refs/remotes)" -- $cur ) )
4444
if [ ${#refs[@]} -eq 0 ]; then

doc/getting-started.rst

+41
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,47 @@ Or tell umpf to rebase onto a new *umpf-base* when creating a fresh *utag*::
447447
# umpf-topic-range: 8bae5bbec8cb4599c141405e9755b7c0e42e064f..19cdc2b857e662a38c712b41ce610000a5ddc6ae
448448
# umpf-end
449449

450+
Synchronizing umpf topic branch
451+
-------------------------------
452+
453+
Due to Git's distributed nature, checked out topic branches can get
454+
out-of-sync. To compare local topic branches against those referenced
455+
in a *utag*, ``umpf pull`` can be used::
456+
457+
umpf --dry-run pull 5.0/special-customer-release/20190311-1
458+
umpf: Using series from commit message...
459+
* [new branch] 02fb74aa381080855a57080138b29ecc96586788 -> v5.0/topic/most-fixes
460+
! [rejected] f0693b782dd026f2adc4d3c336d9ac6dfb352a73 -> v5.0/topic/more-fixes (non-fast-forward)
461+
462+
Following options are supported:
463+
464+
- ``--dry-run``: compare the branches, but stop short of actually updating
465+
them
466+
- ``--force``: reset local branches that are not checked-out to the
467+
``umpf-hashinfo`` in the ``utag``
468+
- ``--update``: restrict updates to only branches available locally
469+
470+
The counterpart to publish topic branches to a remote after creating a new
471+
``utag`` is ``umpf push``:
472+
473+
umpf --dry-run --remote=downstream push 5.0/special-customer-release/20190311-1
474+
umpf: Using series from commit message...
475+
To ssh://downstream
476+
* [new branch] 02fb74aa381080855a57080138b29ecc96586788 -> v5.0/topic/most-fixes
477+
! [rejected] f0693b782dd026f2adc4d3c336d9ac6dfb352a73 -> v5.0/topic/more-fixes (non-fast-forward)
478+
error: failed to push some refs to 'ssh:/downstream'
479+
480+
It supports the same options as ``umpf pull``, but instead of doing local
481+
changes, it operates on the specified remote.
482+
483+
``umpf push`` is especially useful when multiple developers are creating
484+
`utags` for the same project in parallel. Each developer will initially
485+
only push their `utag` to the common repository. Once the changes
486+
introduced by a `utag` are accepted, all topic branches can be force
487+
updated on the remote to this most recent `utag`, possibly via
488+
a server-side pull-request post-merge hook running, e.g.::
489+
490+
umpf --remote=downstream --force .../linux/patches/series.inc
450491

451492
Overview
452493
--------

umpf

+202-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ PATCH_DIR="umpf-patches"
3838
IDENTICAL=false
3939
STABLE=false
4040
FORCE=false
41+
DRYRUN=false
4142
UPDATE=false
4243
VERBOSE=false
4344
VERSION_SEPARATOR=-
@@ -178,6 +179,8 @@ usage() {
178179
--nix with format-patch: write patch series nix
179180
-h, --help
180181
-f, --force
182+
--dry-run with push/pull: Do everything except actually send
183+
the updates.
181184
--flags specify/override umpf-flags
182185
-i, --identical use exact commit hashes, not tip of branches
183186
-s, --stable create a 'stable' tag from a branch based on an
@@ -194,6 +197,7 @@ usage() {
194197
specified, it's interpreted as
195198
<topic>=[<remote>/]<topic>
196199
-u, --update with --patchdir: update existing patches in <path>
200+
with push/pull: update only existing branches
197201
-v, --version <version> with tag: overwrite version number [default: 1]
198202
199203
Commands:
@@ -218,6 +222,8 @@ usage() {
218222
build <umpf> build an umerge from another umpf
219223
distribute <commit-ish> push patches not yet in any topic branch
220224
upstream
225+
push [<umpf>] push topic branches to the given remote
226+
pull [<umpf>] pull topic branches into the local repository
221227
222228
continue continue a previously interrupted umpf command
223229
abort abort a previously started umpf command
@@ -245,7 +251,7 @@ setup() {
245251
fi
246252

247253
o="fhilsub:n:p:r:v:"
248-
l="auto-rerere,bb,nix,flags:,force,help,identical,stable,update,base:,name:,patchdir:,relative:,override:,remote:,local,version:"
254+
l="auto-rerere,bb,nix,flags:,dry-run,force,help,identical,stable,update,base:,name:,patchdir:,relative:,override:,remote:,local,version:"
249255
if ! args="$(getopt -n umpf -o "${o}" -l "${l}" -- "${@}")"; then
250256
usage
251257
exit 1
@@ -271,6 +277,9 @@ setup() {
271277
-f|--force)
272278
FORCE=true
273279
;;
280+
--dry-run)
281+
DRYRUN=true
282+
;;
274283
--flags)
275284
FLAGS="${1}"
276285
shift
@@ -1855,6 +1864,198 @@ do_distribute() {
18551864
run_distribute
18561865
}
18571866
1867+
### namespace: push ###
1868+
1869+
push_topic() {
1870+
echo "${content}" >> "${STATE}/topic-names"
1871+
}
1872+
1873+
push_hashinfo() {
1874+
echo "${content}" >> "${STATE}/topics"
1875+
}
1876+
1877+
push_release() {
1878+
echo "${content}" >> "${STATE}/tagname"
1879+
}
1880+
1881+
push_topic_range() {
1882+
[ ! -e "${STATE}/tagname" ] && return
1883+
[ -e "${STATE}/tagrev-flat" ] && abort "more than one 'topic-range' after 'release'!"
1884+
1885+
echo "${content##*..}" > "${STATE}/tagrev-flat"
1886+
}
1887+
1888+
### command: push ###
1889+
1890+
resolve_commitish() {
1891+
${GIT} rev-parse --revs-only "$@" 2>/dev/null
1892+
}
1893+
1894+
shorten_commitish() {
1895+
resolve_commitish --short ${1}
1896+
}
1897+
1898+
resolve_tag() {
1899+
local remote=$1 tag=$2 commit
1900+
if [ -n "$remote" ]; then
1901+
# handles conflicting tags on remote
1902+
commit=$(git ls-remote -q $remote refs/tags/$tag 2>/dev/null | \
1903+
sed 's/\s\+.*$//')
1904+
else
1905+
commit="refs/tags/$tag"
1906+
fi
1907+
1908+
resolve_commitish "${commit}^{}"
1909+
}
1910+
1911+
update_local() {
1912+
local success=false args="${1}"
1913+
local opts
1914+
1915+
${FORCE} && opts+="--force"
1916+
1917+
local line
1918+
while read -r line; do
1919+
local prefix="" suffix=""
1920+
1921+
eval set -- ${line}
1922+
[ ${#} -eq 0 ] && continue
1923+
1924+
local refparsed=$(shorten_commitish $3)
1925+
1926+
if [ -z "${refparsed}" ]; then
1927+
prefix=" * [new branch]"
1928+
elif [[ "${1}" = *~* ]]; then
1929+
prefix=" ${1}..$(shorten_commitish ${2})"
1930+
elif $FORCE; then
1931+
prefix=" + ${refparsed}..$(shorten_commitish ${2})"
1932+
suffix=" (forced update)"
1933+
else
1934+
prefix=" ! [rejected]"
1935+
suffix=" (non-fast-forward)"
1936+
fi
1937+
1938+
printf "%-40s %s -> %s%s\n" "$prefix" $2 $3 "$suffix"
1939+
done <<< "$args"
1940+
1941+
while read -r line; do
1942+
eval set -- ${line}
1943+
[ ${#} -eq 0 ] && continue
1944+
1945+
if $DRYRUN || git branch $opts $3 $2; then
1946+
success=true
1947+
fi
1948+
done <<< "$args"
1949+
1950+
$success || abort
1951+
}
1952+
1953+
do_push () {
1954+
local opts args remote
1955+
local -a branches branch_names
1956+
local -A topics
1957+
1958+
if [ -z "${GIT_REMOTE}" ]; then
1959+
info "Git remote must be specified. Cannot continue."
1960+
exit 1
1961+
fi
1962+
1963+
if [ "${GIT_REMOTE}" != "refs/heads/" ]; then
1964+
remote=${GIT_REMOTE%/}
1965+
fi
1966+
1967+
prepare_persistent push "${@}"
1968+
parse_series push "${STATE}/series"
1969+
1970+
local tagname="$(<"${STATE}/tagname")"
1971+
local tagrevf="$(<"${STATE}/tagrev-flat")"
1972+
mapfile -t branches < "${STATE}/topics"
1973+
mapfile -t branch_names < "${STATE}/topic-names"
1974+
1975+
if [ -n "${remote}" ]; then
1976+
# Needed, so git rev-parse below can check for existent branches
1977+
git fetch --quiet --no-tags ${remote} 2>/dev/null
1978+
fi
1979+
1980+
local rtagrev="$(resolve_tag "${remote}" ${tagname})"
1981+
local rtagrevf="$(resolve_commitish ${rtagrev}^)"
1982+
if [ "$tagrevf" != "$rtagrevf" ]; then
1983+
if [ -z "$rtagrevf" ]; then
1984+
abort "${remote}${remote:+/}refs/tags/$tagname not found"
1985+
else
1986+
abort "${remote}${remote:+/}refs/tags/$tagname" \
1987+
"has unexpected commit-ish $rtagrev"
1988+
fi
1989+
fi
1990+
1991+
for i in "${!branch_names[@]}"; do
1992+
local branch=${branch_names[$i]}
1993+
local rbranchrev="$(resolve_commitish "${GIT_REMOTE}${branch}")"
1994+
1995+
[ -z "${remote}" ] && [ "${branches[$i]}" = "${rbranchrev}" ] && continue
1996+
1997+
# Don't touch local branches that are already on the correct revision.
1998+
# For remote branches, we let git push handle it.
1999+
[ -z "${remote}" ] && [ "${branches[$i]}" = "${rbranchrev}" ] && continue
2000+
$UPDATE && [ -z "${rbranchrev}" ] && continue
2001+
2002+
topics[${branch}]=$(resolve_commitish ${branches[$i]})
2003+
done
2004+
2005+
if [ -n "${remote}" ]; then
2006+
${FORCE} && opts+="--force-with-lease"
2007+
${DRYRUN} && opts+="--dry-run"
2008+
2009+
for topic in "${!topics[@]}"; do
2010+
args+="${topics[$topic]}:refs/heads/${topic} "
2011+
done
2012+
2013+
if [ -z "$args" ]; then
2014+
info "No branches to push"
2015+
cleanup
2016+
return
2017+
fi
2018+
2019+
${GIT} push $opts ${remote} -- $args
2020+
else # local
2021+
for topic in "${!topics[@]}"; do
2022+
local ref=$topic rev=${topics[$topic]}
2023+
local oldval
2024+
2025+
# The old value being computed below is not needed to
2026+
# create the branch. We compute a suitable one anyway,
2027+
# so we can show how a ref's commit-ish has changed in
2028+
# the pull case like we do in the push case.
2029+
if git merge-base --is-ancestor $ref $rev &>/dev/null; then
2030+
local ancestors=$(git rev-list $rev ^$ref --count)
2031+
args+="$(shorten_commitish "$rev")~$ancestors"
2032+
elif $FORCE; then
2033+
args+="$(shorten_commitish "$ref")"
2034+
else
2035+
args+='""'
2036+
fi
2037+
args+=" $rev $ref"
2038+
args+=$'\n'
2039+
done
2040+
2041+
if [ -z "$args" ]; then
2042+
info "No branches to push"
2043+
cleanup
2044+
return
2045+
fi
2046+
2047+
update_local "$args"
2048+
fi
2049+
2050+
cleanup
2051+
}
2052+
2053+
### command: pull ###
2054+
2055+
do_pull () {
2056+
GIT_REMOTE=refs/heads/ do_push "$@"
2057+
}
2058+
18582059
### command: continue ###
18592060
18602061
do_continue() {

0 commit comments

Comments
 (0)