From 7d0912f0ca8d9d699ae67f291a9d52f9dac96eae 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 | 122 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 163 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..35bd388 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 pull --dry-run 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``: only update existing branches + +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 push --remote=downstream --force .../linux/patches/series.inc Overview -------- diff --git a/umpf b/umpf index 4fbe477..eded88c 100755 --- a/umpf +++ b/umpf @@ -40,6 +40,7 @@ IDENTICAL=false STABLE=false DEFAULT=false FORCE=false +DRYRUN=false UPDATE=false VERBOSE=false VERSION_SEPARATOR=- @@ -182,6 +183,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 @@ -198,6 +201,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: @@ -222,6 +226,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 @@ -249,7 +255,7 @@ setup() { fi o="fhilsub:n:p:r:v:" - l="auto-rerere,bb,nix,flags:,default,force,help,identical,stable,update,base:,name:,patchdir:,relative:,override:,remote:,local,version:" + l="auto-rerere,bb,nix,flags:,default,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 @@ -278,6 +284,9 @@ setup() { -f|--force) FORCE=true ;; + --dry-run) + DRYRUN=true + ;; --flags) FLAGS="${1}" shift @@ -1880,6 +1889,117 @@ do_distribute() { run_distribute } +### namespace: push ### + +push_topic() { + echo "${content}" >> "${STATE}/topic-names" +} + +push_hashinfo() { + echo "${content}" >> "${STATE}/topics" +} + +push_version() { + echo "${content}" > "${STATE}/tagname" +} + +push_topic_range() { + echo "${content##*..}" > "${STATE}/tagrev-flat" +} + +### command: push ### + +shorten_commitish() { + ${GIT} rev-parse --revs-only --short "${@}" 2>/dev/null +} + +do_push () { + local remote + local -a opts args 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_DIR}" + else + 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" + for ((i = 0; i < ${#branch_names[@]}; i++)); do + topics["refs/heads/${branch_names[i]}"]="${branches[i]}" + done + + # To determine whether the remote tag matches what we are about + # to push, we need to fetch it first to look at the flat tag's revision + if ! git fetch --quiet --no-tags "${remote}" "${tagname}" 2>/dev/null; then + abort "${remote}${remote:+/}refs/tags/${tagname} not found" + fi + + local rtagrevf="$(${GIT} rev-parse --revs-only "FETCH_HEAD^" 2>/dev/null)" + if [ "${rtagrevf}" != "${tagrevf}" ]; then + abort "${remote}${remote:+/}refs/tags/${tagname} has unexpected" \ + 'commit hash "'"$(shorten_commitish "${rtagrevf}")"'"' \ + 'instead of "' "$(shorten_commitish "${tagrevf}")" '"' + fi + + if ${UPDATE}; then + local -A rtopics + + exec {lsremotefd}< <(${GIT} ls-remote "${remote}" "${!topics[@]}") + while read sha ref <&${lsremotefd}; do + rtopics["$ref"]="${sha}" + done + exec {lsremotefd}<&- + + # Don't create any new branches, only updated existing ones + for topic in "${!topics[@]}"; do + if [[ ! -v "rtopics[$topic]" ]]; then + unset "topics[$topic]" + fi + done + fi + + if ${FORCE}; then + if [ "${GIT_REMOTE}" = "refs/heads/" ]; then + # Otherwise, we'll always get rejected for "stale info" + opts+=("--force") + else + opts+=("--force-with-lease") + fi + fi + + if ${DRYRUN}; then + opts+=("--dry-run") + fi + + for topic in "${!topics[@]}"; do + args+=("${topics[$topic]}:${topic}") + done + + # Push tag again to avoid an error if $args is empty + ${GIT} push "${opts[@]}" ${remote} -- "${tagname}" "${args[@]}" + + cleanup +} + +### command: pull ### + +do_pull () { + GIT_REMOTE=refs/heads/ do_push "$@" +} + ### command: continue ### do_continue() {