git

git - Fusionar sin cambiar el directorio de trabajo.



(5)

  1. Combinar maestro a tema: git merge origin / master o fusionar tema a sí mismo
  2. Cambiar jefe de maestro: git update-ref refs / heads / master refs / heads / topic

Ahora puede volver al tema para realizar una combinación previa: git reset HEAD ~

Tengo el siguiente escenario:

* 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 al retirar el master, y luego fusionar el tema en él, git cambia mi directorio de trabajo (aunque el resultado final es idéntico al del master), y el problema que tengo con eso se debe al tamaño de nuestro proyecto. se tarda unos 30 minutos en construirlo (con IncrediBuild) aunque nada realmente cambió y es simplemente insoportable.

Entonces, lo que me gustaría obtener es lo siguiente:

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

Sin realmente tocar el directorio de trabajo (o al menos engañar a Git de alguna manera).


¡Interesante! No creo que haya una forma integrada de hacer esto, pero deberías poder manipularlo usando la tubería:

#!/bin/bash branch=master # or take an argument: # if [ $@ 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 ''s@.*/@@'') # 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

Lo interesante es que newcommit= line; utiliza commit-tree para crear directamente la confirmación de fusión. El primer argumento es el árbol a utilizar; Esa es la CABEZA del árbol, la rama cuyo contenido desea conservar. El mensaje de confirmación se proporciona en la entrada estándar, y el resto de los argumentos nombran a los padres que debe tener la nueva confirmación. El SHA1 de la confirmación se imprime en la salida estándar, por lo que, suponiendo que la confirmación se haya realizado correctamente, la captura, luego fusiona esa confirmación (será un avance rápido). Si eres obsesivo, puedes asegurarte de que el árbol de compromiso haya tenido éxito, pero eso debería estar bastante garantizado.

Limitaciones:

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

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


Alternativamente, puede corregir los síntomas directamente guardando y restaurando las marcas de tiempo de los archivos. Esto es un poco feo, pero fue interesante escribir.

Guión de guardar / restaurar marca de tiempo de Python

#!/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()

Bash Test Script

#!/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 un ejercicio para que el lector limpie el script de Python para eliminar resultados extraños y agregar una mejor comprobación de errores.


Aquí hay una especie de versión engañosa.

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

La forma más sencilla en la que puedo pensar sería git clone en una copia de trabajo por separado, hacer la fusión allí, luego git pull retroceder. 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, hacer clones temporales no es lo ideal, y necesita una buena cantidad de espacio extra en el disco duro. El costo de tiempo del clon adicional se puede minimizar (a largo plazo) manteniendo su copia de fusión, siempre y cuando no necesite el espacio en el disco.

Descargo de responsabilidad: no he verificado que esto funciona. Aunque creo que debería (git no hace la versión de las marcas de tiempo del archivo)