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..ff74e85 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() {