Come iterare attraverso tutti i rami git usando lo script bash


105

Come posso iterare attraverso tutti i rami locali nel mio repository usando lo script bash. Ho bisogno di iterare e controllare se c'è qualche differenza tra il ramo e alcuni rami remoti. Ex

for branch in $(git branch); 
do
    git log --oneline $branch ^remotes/origin/master;
done

Devo fare qualcosa come indicato sopra, ma il problema che sto affrontando è $ (git branch) che mi fornisce le cartelle all'interno della cartella del repository insieme ai rami presenti nel repository.

È questo il modo corretto per risolvere questo problema? O c'è un altro modo per farlo?

Grazie



1
@pihentagy Questo è stato scritto prima della domanda collegata.
kodaman

Ad esempio: -for b in "$(git branch)"; do git branch -D $b; done
Abhi

Risposte:


156

Non dovresti usare git branch quando scrivi script. Git fornisce un'interfaccia "idraulica" progettata esplicitamente per l'uso nello scripting (molte implementazioni attuali e storiche dei normali comandi Git (aggiungi, checkout, merge, ecc.) Utilizzano questa stessa interfaccia).

Il comando idraulico che desideri è git for-each-ref :

git for-each-ref --shell \
  --format='git log --oneline %(refname) ^origin/master' \
  refs/heads/

Nota: non è necessario il remotes/prefisso sul riferimento remoto a meno che non si disponga di altri riferimenti che fanno origin/mastercorrispondere più posizioni nel percorso di ricerca del nome del riferimento (vedere "Un nome di riferimento simbolico ..." nella sezione Specifica delle revisioni di git-rev-parse (1) ). Se si sta cercando di evitare esplicitamente l'ambiguità, poi andare con il nome completo ref: refs/remotes/origin/master.

Otterrai un output come questo:

git log --oneline 'refs/heads/master' ^origin/master
git log --oneline 'refs/heads/other' ^origin/master
git log --oneline 'refs/heads/pu' ^origin/master

Puoi reindirizzare questo output in sh .

Se non ti piace l'idea di generare il codice della shell, potresti rinunciare a un po 'di robustezza * e fare questo:

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    git log --oneline "$branch" ^origin/master
done

* I nomi dei riferimenti dovrebbero essere al sicuro dalla suddivisione delle parole della shell (vedere git-check-ref-format (1) ). Personalmente mi atterrei alla versione precedente (codice shell generato); Sono più fiducioso che non possa accadere nulla di inappropriato.

Dato che hai specificato bash e supporta gli array, puoi mantenere la sicurezza ed evitare comunque di generare il coraggio del tuo ciclo:

branches=()
eval "$(git for-each-ref --shell --format='branches+=(%(refname))' refs/heads/)"
for branch in "${branches[@]}"; do
    # …
done

Potresti fare qualcosa di simile $@se non stai usando una shell che supporta gli array ( set --per inizializzare e set -- "$@" %(refname)aggiungere elementi).


39
Sul serio. Non c'è un modo più semplice per farlo?
Jim Fell

4
Ma cosa succede se voglio usare una delle opzioni di filtro di git branch, come --merged, dovrei duplicare la logica in git branch? Ci deve essere un modo migliore per farlo.
Thayne

3
Versione più semplice:git for-each-ref refs/heads | cut -d/ -f3-
WID

4
@wid: O, semplicemente,git for-each-ref refs/heads --format='%(refname)'
John Gietzen

5
@Thayne: questa domanda è vecchia, ma la gente di Git ha finalmente risolto il problema: for-each-refora supporta tutti i selettori di ramo come --mergede git branche git tagsono ora effettivamente implementati in termini di git for-each-refse stesso, almeno per i casi esistenti nell'elenco. (La creazione di nuovi rami e tag non è e non dovrebbe farne parte for-each-ref.)
torek

50

Questo perché git branchcontrassegna il ramo corrente con un asterisco, ad esempio:

$ git branch
* master
  mybranch
$ 

così si $(git branch)espande ad esempio * master mybranch, e poi si *espande all'elenco dei file nella directory corrente.

Non vedo un'opzione ovvia per non stampare l'asterisco in primo luogo; ma potresti tagliarlo:

$(git branch | cut -c 3-)

4
Se racchiudi tra virgolette doppie puoi impedire a bash di espandere l'asterisco, anche se vorrai comunque rimuoverlo dall'output. Un modo più efficace per rimuovere un asterisco da qualsiasi punto sarebbe $(git branch | sed -e s/\\*//g).
Nick

2
bello, mi piace molto la tua 3-soluzione.
Andrei-Niculae Petre

1
Versione sed leggermente più semplice:$(git branch | sed 's/^..//')
jdg

5
versione tr leggermente più semplice:$(git branch | tr -d " *")
ccpizza

13

Il bash builtin,, mapfileè costruito per questo

tutti i rami git: git branch --all --format='%(refname:short)'

tutti i rami git locali: git branch --format='%(refname:short)'

tutti i rami git remoti: git branch --remotes --format='%(refname:short)'

itera attraverso tutti i rami di git: mapfile -t -C my_callback -c 1 < <( get_branches )

esempio:

my_callback () {
  INDEX=${1}
  BRANCH=${2}
  echo "${INDEX} ${BRANCH}"
}
get_branches () {
  git branch --all --format='%(refname:short)'
}
# mapfile -t -C my_callback -c 1 BRANCHES < <( get_branches ) # if you want the branches that were sent to mapfile in a new array as well
# echo "${BRANCHES[@]}"
mapfile -t -C my_callback -c 1 < <( get_branches )

per la situazione specifica del PO:

#!/usr/bin/env bash


_map () {
  ARRAY=${1?}
  CALLBACK=${2?}
  mapfile -t -C "${CALLBACK}" -c 1 <<< "${ARRAY[@]}"
}


get_history_differences () {
  REF1=${1?}
  REF2=${2?}
  shift
  shift
  git log --oneline "${REF1}" ^"${REF2}" "${@}"
}


has_different_history () {
  REF1=${1?}
  REF2=${2?}
  HIST_DIFF=$( get_history_differences "${REF1}" "${REF2}" )
  return $( test -n "${HIST_DIFF}" )
}


print_different_branches () {
  read -r -a ARGS <<< "${@}"
  LOCAL=${ARGS[-1]?}
  for REMOTE in "${SOME_REMOTE_BRANCHES[@]}"; do
    if has_different_history "${LOCAL}" "${REMOTE}"; then
      # { echo; echo; get_history_differences "${LOCAL}" "${REMOTE}" --color=always; } # show differences
      echo local branch "${LOCAL}" is different than remote branch "${REMOTE}";
    fi
  done
}


get_local_branches () {
  git branch --format='%(refname:short)'
}


get_different_branches () {
  _map "$( get_local_branches )" print_different_branches
}


# read -r -a SOME_REMOTE_BRANCHES <<< "${@}" # use this instead for command line input
declare -a SOME_REMOTE_BRANCHES
SOME_REMOTE_BRANCHES=( origin/master remotes/origin/another-branch another-remote/another-interesting-branch )
DIFFERENT_BRANCHES=$( get_different_branches )

echo "${DIFFERENT_BRANCHES}"

fonte: elenca tutti i rami git locali senza asterisco


5

Io itero come per esempio:

for BRANCH in `git branch --list|sed 's/\*//g'`;
  do 
    git checkout $BRANCH
    git fetch
    git branch --set-upstream-to=origin/$BRANCH $BRANCH
  done
git checkout master;

4

Suggerirei $(git branch|grep -o "[0-9A-Za-z]\+")se le filiali locali sono denominate solo con cifre, az e / o lettere AZ


4

La risposta accettata è corretta e dovrebbe essere l'approccio utilizzato, ma risolvere il problema in bash è un ottimo esercizio per capire come funzionano le shell. Il trucco per farlo usando bash senza eseguire ulteriori manipolazioni del testo, è assicurarsi che l'output di git branch non venga mai espanso come parte di un comando che deve essere eseguito dalla shell. Ciò impedisce all'asterisco di espandersi nell'espansione del nome file (passaggio 8) dell'espansione della shell (vedere http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_04.html )

Usa la costruzione bash while con un comando di lettura per tagliare l'output del ramo git in righe. L '"*" verrà letto come un carattere letterale. Usa un'istruzione case per abbinarla, prestando particolare attenzione ai modelli di corrispondenza.

git branch | while read line ; do                                                                                                        
    case $line in
        \*\ *) branch=${line#\*\ } ;;  # match the current branch
        *) branch=$line ;;             # match all the other branches
    esac
    git log --oneline $branch ^remotes/origin/master
done

Gli asterischi sia nel costrutto bash case che nella sostituzione del parametro devono essere preceduti da barre rovesciate per evitare che la shell li interpreti come caratteri di corrispondenza del modello. Anche gli spazi vengono sottoposti a escape (per evitare la tokenizzazione) perché stai letteralmente abbinando "*".


4

L'opzione più semplice da ricordare secondo me:

git branch | grep "[^* ]+" -Eo

Produzione:

bamboo
develop
master

L'opzione -o di Grep (--only-matching) limita l'output solo alle parti corrispondenti dell'input.

Poiché né lo spazio né * sono validi nei nomi dei rami Git, questo restituisce l'elenco dei rami senza i caratteri aggiuntivi.

Modifica: se sei nello stato "testa scollegata" , dovrai filtrare la voce corrente:

git branch --list | grep -v "HEAD detached" | grep "[^* ]+" -oE


3

Quello che ho finito per fare, applicato alla tua domanda (e ispirato dalla menzione di ccpizza tr):

git branch | tr -d ' *' | while IFS='' read -r line; do git log --oneline "$line" ^remotes/origin/master; done

(Uso molto i cicli while. Mentre per cose particolari vorresti sicuramente usare un nome di variabile appuntito ["ramo", ad esempio], il più delle volte mi occupo solo di fare qualcosa con ogni riga di input. 'linea' qui invece di 'ramo' è un cenno alla riusabilità / memoria muscolare / efficienza.)


1

Estendendo dalla risposta di @finn (grazie!), Quanto segue ti consentirà di iterare sui rami senza creare uno script di shell intermedio. È abbastanza robusto, a condizione che non ci siano nuove righe nel nome del ramo :)

git for-each-ref --format='%(refname)' refs/heads  | while read x ; do echo === $x === ; done

Il ciclo while viene eseguito in una subshell, il che di solito va bene a meno che non si impostino variabili di shell a cui si desidera accedere nella shell corrente. In tal caso si utilizza la sostituzione del processo per invertire il tubo:

while read x ; do echo === $x === ; done < <( git for-each-ref --format='%(refname)' refs/heads )

1

Se sei in questo stato:

git branch -a

* master

  remotes/origin/HEAD -> origin/master

  remotes/origin/branch1

  remotes/origin/branch2

  remotes/origin/branch3

  remotes/origin/master

Ed esegui questo codice:

git branch -a | grep remotes/origin/*

for BRANCH in `git branch -a | grep remotes/origin/*` ;

do
    A="$(cut -d'/' -f3 <<<"$BRANCH")"
    echo $A

done        

Otterrai questo risultato:

branch1

branch2

branch3

master

Questa mi sembra una soluzione meno complessa +1
Abhi

Questo ha funzionato anche per me:for b in "$(git branch)"; do git branch -D $b; done
Abhi

1

Sii semplice

Il modo semplice per ottenere il nome del ramo in loop usando lo script bash.

#!/bin/bash

for branch in $(git for-each-ref --format='%(refname)' refs/heads/); do
    echo "${branch/'refs/heads/'/''}" 
done

Produzione:

master
other

1

La risposta di Googlian, ma senza usare per

git for-each-ref --format='%(refname:lstrip=-1)' refs/heads/

1
Questo non funziona per i nomi dei rami con spazio dei nomi, come nei rami che hanno una barra. Ciò significa che i rami creati da dependabot, che assomigliano a qualcosa come "dependabot / npm_and_yarn / typescript-3.9.5", appariranno invece come "typescript-3.9.5".
ecbrodie

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.