From b48609e1c3ac2055202d8fd4be0cb5b1ba5ae40d Mon Sep 17 00:00:00 2001 From: Ahmad Fatoum Date: Sat, 25 Jan 2025 10:48:21 +0100 Subject: [PATCH] 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 --- bash_completion | 2 +- doc/getting-started.rst | 41 ++++++++ umpf | 202 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 243 insertions(+), 2 deletions(-) diff --git a/bash_completion b/bash_completion index 5313106..efda508 100644 --- a/bash_completion +++ b/bash_completion @@ -38,7 +38,7 @@ _umpf_completion() "") COMPREPLY=( $( compgen -W "${completion_cmds[*]} help" -- $cur ) ) ;; - diff|show|tag|tig|build) + diff|show|tag|tig|build|push|pull) local -a refs refs=( $( compgen -W "$( git for-each-ref --format='%(refname:short)' refs/tags refs/heads refs/remotes)" -- $cur ) ) if [ ${#refs[@]} -eq 0 ]; then diff --git a/doc/getting-started.rst b/doc/getting-started.rst index d98c079..6a69a88 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -447,6 +447,47 @@ Or tell umpf to rebase onto a new *umpf-base* when creating a fresh *utag*:: # umpf-topic-range: 8bae5bbec8cb4599c141405e9755b7c0e42e064f..19cdc2b857e662a38c712b41ce610000a5ddc6ae # umpf-end +Synchronizing umpf topic branch +------------------------------- + +Due to Git's distributed nature, checked out topic branches can get +out-of-sync. To compare local topic branches against those referenced +in a *utag*, ``umpf pull`` can be used:: + + umpf --dry-run pull 5.0/special-customer-release/20190311-1 + umpf: Using series from commit message... + * [new branch] 02fb74aa381080855a57080138b29ecc96586788 -> v5.0/topic/most-fixes + ! [rejected] f0693b782dd026f2adc4d3c336d9ac6dfb352a73 -> v5.0/topic/more-fixes (non-fast-forward) + +Following options are supported: + +- ``--dry-run``: compare the branches, but stop short of actually updating + them +- ``--force``: reset local branches that are not checked-out to the + ``umpf-hashinfo`` in the ``utag`` +- ``--update``: restrict updates to only branches available locally + +The counterpart to publish topic branches to a remote after creating a new +``utag`` is ``umpf push``: + + umpf --dry-run --remote=downstream push 5.0/special-customer-release/20190311-1 + umpf: Using series from commit message... + To ssh://downstream + * [new branch] 02fb74aa381080855a57080138b29ecc96586788 -> v5.0/topic/most-fixes + ! [rejected] f0693b782dd026f2adc4d3c336d9ac6dfb352a73 -> v5.0/topic/more-fixes (non-fast-forward) + error: failed to push some refs to 'ssh:/downstream' + +It supports the same options as ``umpf pull``, but instead of doing local +changes, it operates on the specified remote. + +``umpf push`` is especially useful when multiple developers are creating +`utags` for the same project in parallel. Each developer will initially +only push their `utag` to the common repository. Once the changes +introduced by a `utag` are accepted, all topic branches can be force +updated on the remote to this most recent `utag`, possibly via +a server-side pull-request post-merge hook running, e.g.:: + + umpf --remote=downstream --force .../linux/patches/series.inc Overview -------- diff --git a/umpf b/umpf index 2372ec3..9a37246 100755 --- a/umpf +++ b/umpf @@ -38,6 +38,7 @@ PATCH_DIR="umpf-patches" IDENTICAL=false STABLE=false FORCE=false +DRYRUN=false UPDATE=false VERBOSE=false VERSION_SEPARATOR=- @@ -178,6 +179,8 @@ usage() { --nix with format-patch: write patch series nix -h, --help -f, --force + --dry-run with push/pull: Do everything except actually send + the updates. --flags specify/override umpf-flags -i, --identical use exact commit hashes, not tip of branches -s, --stable create a 'stable' tag from a branch based on an @@ -194,6 +197,7 @@ usage() { specified, it's interpreted as =[/] -u, --update with --patchdir: update existing patches in + with push/pull: update only existing branches -v, --version with tag: overwrite version number [default: 1] Commands: @@ -218,6 +222,8 @@ usage() { build build an umerge from another umpf distribute push patches not yet in any topic branch upstream + push [] push topic branches to the given remote + pull [] pull topic branches into the local repository continue continue a previously interrupted umpf command abort abort a previously started umpf command @@ -245,7 +251,7 @@ setup() { fi o="fhilsub:n:p:r:v:" - l="auto-rerere,bb,nix,flags:,force,help,identical,stable,update,base:,name:,patchdir:,relative:,override:,remote:,local,version:" + l="auto-rerere,bb,nix,flags:,dry-run,force,help,identical,stable,update,base:,name:,patchdir:,relative:,override:,remote:,local,version:" if ! args="$(getopt -n umpf -o "${o}" -l "${l}" -- "${@}")"; then usage exit 1 @@ -271,6 +277,9 @@ setup() { -f|--force) FORCE=true ;; + --dry-run) + DRYRUN=true + ;; --flags) FLAGS="${1}" shift @@ -1855,6 +1864,197 @@ do_distribute() { run_distribute } +### namespace: push ### + +push_topic() { + echo "${content}" >> "${STATE}/topic-names" +} + +push_hashinfo() { + echo "${content}" >> "${STATE}/topics" +} + +push_release() { + echo "${content}" >> "${STATE}/tagname" +} + +push_topic_range() { + [ ! -e "${STATE}/tagname" ] && return + [ -e "${STATE}/tagrev-flat" ] && abort "more than one 'topic-range' after 'release'!" + + echo "${content##*..}" > "${STATE}/tagrev-flat" +} + +### command: push ### + +resolve_commitish() { + ${GIT} rev-parse --revs-only "$@" 2>/dev/null +} + +shorten_commitish() { + resolve_commitish --short ${1} +} + +resolve_tag() { + local remote=$1 tag=$2 commit + if [ -n "$remote" ]; then + # handles conflicting tags on remote + commit=$(git ls-remote -q $remote refs/tags/$tag 2>/dev/null | \ + sed 's/\s\+.*$//') + else + commit="refs/tags/$tag" + fi + + resolve_commitish "${commit}^{}" +} + +update_local() { + local success=false args="${1}" + local opts + + ${FORCE} && opts+="--force" + + local line + while read -r line; do + local prefix="" suffix="" + + eval set -- ${line} + [ ${#} -eq 0 ] && continue + + local refparsed=$(shorten_commitish $3) + + if [ -z "${refparsed}" ]; then + prefix=" * [new branch]" + elif [[ "${1}" = *~* ]]; then + prefix=" ${1}..$(shorten_commitish ${2})" + elif $FORCE; then + prefix=" + ${refparsed}..$(shorten_commitish ${2})" + suffix=" (forced update)" + else + prefix=" ! [rejected]" + suffix=" (non-fast-forward)" + fi + + printf "%-40s %s -> %s%s\n" "$prefix" $2 $3 "$suffix" + done <<< "$args" + + while read -r line; do + eval set -- ${line} + [ ${#} -eq 0 ] && continue + + if $DRYRUN || git branch $opts $3 $2; then + success=true + fi + done <<< "$args" + + $success || abort +} + +do_push () { + local opts args remote + local -a branches branch_names + local -A topics + + if [ -z "${GIT_REMOTE}" ]; then + info "Git remote must be specified. Cannot continue." + exit 1 + fi + + if [ "${GIT_REMOTE}" != "refs/heads/" ]; then + remote=${GIT_REMOTE%/} + fi + + prepare_persistent push "${@}" + parse_series push "${STATE}/series" + + local tagname="$(<"${STATE}/tagname")" + local tagrevf="$(<"${STATE}/tagrev-flat")" + mapfile -t branches < "${STATE}/topics" + mapfile -t branch_names < "${STATE}/topic-names" + + if [ -n "${remote}" ]; then + # Needed, so git rev-parse below can check for existent branches + git fetch --quiet --no-tags ${remote} 2>/dev/null + fi + + local rtagrev="$(resolve_tag "${remote}" ${tagname})" + local rtagrevf="$(resolve_commitish ${rtagrev}^)" + if [ "$tagrevf" != "$rtagrevf" ]; then + if [ -z "$rtagrevf" ]; then + abort "${remote}${remote:+/}refs/tags/$tagname not found" + else + abort "${remote}${remote:+/}refs/tags/$tagname" \ + "has unexpected commit-ish $rtagrev" + fi + fi + + for i in "${!branch_names[@]}"; do + local branch=${branch_names[$i]} + local rbranchrev="$(resolve_commitish "${GIT_REMOTE}${branch}")" + + [ -z "${remote}" ] && [ "${branches[$i]}" = "${rbranchrev}" ] && continue + + # Don't touch local branches that are already on the correct revision. + # For remote branches, we let git push handle it. + [ -z "${remote}" ] && [ "${branches[$i]}" = "${rbranchrev}" ] && continue + $UPDATE && [ -z "${rbranchrev}" ] && continue + + topics[${branch}]=$(resolve_commitish ${branches[$i]}) + done + + if [ -n "${remote}" ]; then + ${FORCE} && opts+="--force-with-lease" + ${DRYRUN} && opts+="--dry-run" + + for topic in "${!topics[@]}"; do + args+="${topics[$topic]}:refs/heads/${topic} " + done + + if [ -z "$args" ]; then + info "No branches to push" + cleanup + return + fi + + ${GIT} push $opts ${remote} -- $args + else # local + for topic in "${!topics[@]}"; do + local ref=$topic rev=${topics[$topic]} + + # The old value being computed below is not needed to + # create the branch. We compute a suitable one anyway, + # so we can show how a ref's commit-ish has changed in + # the pull case like we do in the push case. + if git merge-base --is-ancestor $ref $rev &>/dev/null; then + local ancestors=$(git rev-list $rev ^$ref --count) + args+="$(shorten_commitish "$rev")~$ancestors" + elif $FORCE; then + args+="$(shorten_commitish "$ref")" + else + args+='""' + fi + args+=" $rev $ref" + args+=$'\n' + done + + if [ -z "$args" ]; then + info "No branches to push" + cleanup + return + fi + + update_local "$args" + fi + + cleanup +} + +### command: pull ### + +do_pull () { + GIT_REMOTE=refs/heads/ do_push "$@" +} + ### command: continue ### do_continue() {