2010-08-04 23 views
14

tengo el siguiente escenario:sin cambiar el directorio de trabajo

* ab82147 (HEAD, topic) changes 
* 8993636 changes 
* 82f4426 changes 
* 18be5a3 (master) first 

me gustaría fusionar (no avance rápido) topic en master. Esto me obliga a:

  • git checkout master
  • git merge --no-ff topic

Pero el registro de salida principal, y luego fusionar tema en él provoca git para cambiar mi directorio de trabajo (aunque el resultado final es idéntico al que se antes de echarle un vistazo al master), y el problema que tengo se debe al tamaño de nuestro proyecto, lleva unos 30 minutos construirlo (con IncrediBuild) aunque nada realmente cambió y es simplemente insoportable.

Entonces, ¿qué me gustaría conseguir es la siguiente:

* 9075cf4 (HEAD, master) Merge branch 'topic' 
|\ 
| * ab82147 (topic) changes 
| * 8993636 changes 
| * 82f4426 changes 
|/ 
* 18be5a3 first 

sin realmente tocar el directorio de trabajo (o por lo menos hacer trampa git de alguna manera).

+0

Parece que su acumulación de cadena se ha roto. ¿En qué paso tienes que reconstruir? – cmcginty

+1

@Casey: no, la cadena de construcción está bien. Considere un archivo que fue cambiado entre el maestro y el tema. Cuando revisa el maestro, se lo cambia a la versión del maestro y su marca de tiempo se actualiza. A continuación, fusiona el tema y lo vuelve a cambiar a la versión original, pero todavía se ha tocado, por lo que debe reconstruirse su producto. – Cascabel

+1

Para una fusión de avance rápido sin salida, consulte '(1)': [Git checkout-and-merge sin tocar el árbol de trabajo] (http://stackoverflow.com/q/1402993/456814), y '(2) '' ¿Actualizar/extraer una rama local de Git sin verificarlo?] (Http://stackoverflow.com/q/3216360/456814). –

Respuesta

8

¡Interesante! No creo que hay una manera integrada para hacer esto, pero usted debería ser capaz de falsificar que el uso de la instalación de cañerías:

#!/bin/bash 

branch=master 
# or take an argument: 
# if [ [email protected] eq 1 ]; 
#  branch="$1"; 
# fi 

# make sure the branch exists 
if ! git rev-parse --verify --quiet --heads "$branch" > /dev/null; then 
    echo "error: branch $branch does not exist" 
    exit 1 
fi 

# make sure this could be a fast-forward 
if [ "$(git merge-base HEAD $branch)" == "$(git rev-parse $branch)" ]; then 
    # find the branch name associated with HEAD 
    currentbranch=$(git symbolic-ref HEAD | sed '[email protected]*/@@') 
    # make the commit 
    newcommit=$(echo "Merge branch '$currentbranch'" | git commit-tree $(git log -n 1 --pretty=%T HEAD) -p $branch -p HEAD) 
    # move the branch to point to the new commit 
    git update-ref -m "merge $currentbranch: Merge made by simulated no-ff" "refs/heads/$branch" $newcommit 
else 
    echo "error: merging $currentbranch into $branch would not be a fast-forward" 
    exit 1 
fi 

La parte interesante es que newcommit= línea; usa commit-tree para crear directamente el commit de fusión. El primer argumento es el árbol a usar; ese es el árbol HEAD, la rama cuyo contenido quieres conservar. El mensaje de compromiso se proporciona en stdin, y el resto de los argumentos nombran a los padres que debe tener la nueva confirmación. El SHA1 de compromiso se imprime en stdout, por lo que, suponiendo que la confirmación se realizó correctamente, usted captura eso, luego fusiona ese compromiso (que será un avance rápido). Si eres obsesivo, puedes asegurarte de que commit-tree tenga éxito, pero eso debería estar garantizado.

Limitaciones:

  • esto sólo funciona en combinaciones que podría haber sido un avance rápido. Obviamente, tendrá que verificar y fusionar (posiblemente en un clon, para guardar su sistema de compilación) en ese caso.
  • El mensaje de reflog es diferente. Hice esto deliberadamente, porque cuando usas --no-ff, git se obliga a usar la estrategia predeterminada (recursiva), pero escribir eso en el reflog sería mentira.
  • Si está en el modo HEAD desconectado, las cosas irán mal. Eso debería ser tratado especialmente.

Y sí, ¡probé esto en un repositorio de juguetes, y parece funcionar correctamente! (Aunque no me esforcé para romperlo.)

+0

¡Estoy impresionado por este git vudú! Lo intenté en el repositorio que tenía en la pregunta y los resultados fueron bastante extraños ;-) Así es como se veía el gráfico después: http://paste.lisp.org/+2FCI –

+0

@Idan K: Eso parece exactamente lo que querías, excepto que tienes que ver maestro todavía, ¿sí? – Cascabel

+0

@Jefromi: mire cuidadosamente el '*' que debería pertenecer al tema. Están en la línea izquierda en lugar de la derecha. Una fusión normal produce este gráfico: http://paste.lisp.org/+2FCS –

3

La manera más simple que puedo pensar sería en git clone en una copia de trabajo separada, realice la fusión allí, luego git pull atrás. La extracción será un avance rápido y solo debería afectar a los archivos que realmente han cambiado.

Por supuesto, con un proyecto tan grande que hace clones temporales no es ideal, y necesita un buen pedazo de espacio extra en el disco duro. El costo de tiempo del clon extra se puede minimizar (a largo plazo) al mantener su copia fusionada, siempre que no necesite espacio en el disco.

Descargo de responsabilidad: No he verificado que esto funcione. Aunque creo que debería hacerlo (git no contiene las marcas de tiempo del archivo)

+1

La clonación en una máquina local puede usar enlaces duros o incluso un directorio de objetos compartido. Esto ahorrará mucho espacio. – siride

+0

¿Es difícil vincular los archivos de copia de trabajo reales, o simplemente los objetos de repositorio? Además, ¿esto es cierto en Windows? (La pregunta original menciona a IncrediBuild, entonces asumo que Windows ... probablemente msysGit) –

+3

Definitivamente no vincula rígidamente los archivos del árbol de trabajo: ¿cuál sería el sentido de tener un clon si todo fuera el mismo? – Cascabel

0

Como alternativa, puede corregir los síntomas directamente guardando y restableciendo las marcas de tiempo del archivo. Esto es feo, pero fue interesante escribir.

Python marca de hora Guardar/Restaurar Guión Guión

#!/usr/bin/env python 

from optparse import OptionParser 
import os 
import subprocess 
import cPickle as pickle 

try: 
    check_output = subprocess.check_output 
except AttributeError: 
    # check_output was added in Python 2.7, so it's not always available 
    def check_output(*args, **kwargs): 
     kwargs['stdout'] = subprocess.PIPE 
     proc = subprocess.Popen(*args, **kwargs) 
     output = proc.stdout.read() 
     retcode = proc.wait() 
     if retcode != 0: 
      cmd = kwargs.get('args') 
      if cmd is None: 
       cmd = args[0] 
      err = subprocess.CalledProcessError(retcode, cmd) 
      err.output = output 
      raise err 
     else: 
      return output 

def git_cmd(*args): 
    return check_output(['git'] + list(args), stderr=subprocess.STDOUT) 

def walk_git_tree(rev): 
    """ Generates (sha1,path) pairs for all blobs (files) listed by git ls-tree. """ 
    tree = git_cmd('ls-tree', '-r', '-z', rev).rstrip('\0') 
    for entry in tree.split('\0'): 
     print entry 
     mode, type, sha1, path = entry.split() 
     if type == 'blob': 
      yield (sha1, path) 
     else: 
      print 'WARNING: Tree contains a non-blob.' 

def collect_timestamps(rev): 
    timestamps = {} 
    for sha1, path in walk_git_tree(rev): 
     s = os.lstat(path) 
     timestamps[path] = (sha1, s.st_mtime, s.st_atime) 
     print sha1, s.st_mtime, s.st_atime, path 
    return timestamps 

def restore_timestamps(timestamps): 
    for path, v in timestamps.items(): 
     if os.path.isfile(path): 
      sha1, mtime, atime = v 
      new_sha1 = git_cmd('hash-object', '--', path).strip() 
      if sha1 == new_sha1: 
       print 'Restoring', path 
       os.utime(path, (atime, mtime)) 
      else: 
       print path, 'has changed (not restoring)' 
     elif os.path.exists(path): 
      print 'WARNING: File is no longer a file...' 

def main(): 
    oparse = OptionParser() 
    oparse.add_option('--save', 
     action='store_const', const='save', dest='action', 
     help='Save the timestamps of all git tracked files') 
    oparse.add_option('--restore', 
     action='store_const', const='restore', dest='action', 
     help='Restore the timestamps of git tracked files whose sha1 hashes have not changed') 
    oparse.add_option('--db', 
     action='store', dest='database', 
     help='Specify the path to the data file to restore/save from/to') 

    opts, args = oparse.parse_args() 
    if opts.action is None: 
     oparse.error('an action (--save or --restore) must be specified') 

    if opts.database is None: 
     repo = git_cmd('rev-parse', '--git-dir').strip() 
     dbpath = os.path.join(repo, 'TIMESTAMPS') 
     print 'Using default database:', dbpath 
    else: 
     dbpath = opts.database 

    rev = git_cmd('rev-parse', 'HEAD').strip() 
    print 'Working against rev', rev 

    if opts.action == 'save': 
     timestamps = collect_timestamps(rev) 
     data = (rev, timestamps) 
     pickle.dump(data, open(dbpath, 'wb')) 
    elif opts.action == 'restore': 
     rev, timestamps = pickle.load(open(dbpath, 'rb')) 
     restore_timestamps(timestamps) 

if __name__ == '__main__': 
    main() 

prueba Bash

#!/bin/bash 

if [ -d working ]; then 
    echo "Cowardly refusing to mangle an existing 'working' dir." 
    exit 1 
fi 

mkdir working 
cd working 

# create the repository/working copy 
git init 

# add a couple of files 
echo "File added in master:r1." > file-1 
echo "File added in master:r1." > file-2 
mkdir dir 
echo "File added in master:r1." > dir/file-3 
git add file-1 file-2 dir/file-3 
git commit -m "r1: add-1, add-2, add-3" 
git tag r1 
# sleep to ensure new or changed files won't have the same timestamp 
echo "Listing at r1" 
ls --full-time 
sleep 5 

# make a change 
echo "File changed in master:r2." > file-2 
echo "File changed in master:r2." > dir/file-3 
echo "File added in master:r2." > file-4 
git add file-2 dir/file-3 file-4 
git commit -m "r2: change-2, change-3, add-4" 
git tag r2 
# sleep to ensure new or changed files won't have the same timestamp 
echo "Listing at r2" 
ls --full-time 
sleep 5 

# create a topic branch from r1 and make some changes 
git checkout -b topic r1 
echo "File changed in topic:r3." > file-2 
echo "File changed in topic:r3." > dir/file-3 
echo "File added in topic:r3." > file-5 
git add file-2 dir/file-3 file-5 
git commit -m "r3: change-2, change-3, add-5" 
git tag r3 
# sleep to ensure new or changed files won't have the same timestamp 
echo "Listing at r3" 
ls --full-time 
sleep 5 

echo "Saving timestamps" 
../save-timestamps.py --save 

echo "Checking out master and merging" 
# merge branch 'topic' 
git checkout master 
git merge topic 
echo "File changed in topic:r3." > file-2 # restore file-2 
echo "File merged in master:r4." > dir/file-3 
git add file-2 dir/file-3 
git commit -m "r4: Merge branch 'topic'" 
git tag r4 
echo "Listing at r4" 
ls --full-time 

echo "Restoring timestamps" 
../save-timestamps.py --restore 
ls --full-time 

lo dejaré como ejercicio para el lector para limpiar la secuencia de comandos de Python para eliminar salida extraña y agregar una mejor verificación de errores.

0

Aquí hay una especie de versión infiel.

  1. git alijo
  2. git tag tmptag
  3. git merge --no-ff tema
  4. tmptag git checkout (tha_brunch -b)?
  5. git alijo pop
  6. git tag -D tmptag
Cuestiones relacionadas