From d9ee2ab501089e0ee22305ae99e13c7b730bc798 Mon Sep 17 00:00:00 2001 From: Rob McDonald Date: Tue, 14 Apr 2026 09:34:03 -0700 Subject: [PATCH 001/436] gitk: add horizontal scrollbar to the commit list pane When many branches and tags decorate the same commit, the ref labels push the commit description far to the right, often out of the visible area of the left pane. The canvas widget already tracked the maximum x extent via canvxmax and updated its scrollregion accordingly, but there was no scrollbar wired up to let the user reach that content. Add a horizontal scrollbar (.tf.histframe.cxsb) below the three-pane history area, connected to the left canvas (canv) via its xscrollcommand and xview. No behaviour is changed for the author (canv2) or date (canv3) panes; they continue to scroll only vertically in lock-step as before. Co-Authored-By: Claude Sonnet 4.6 Signed-off-by: Rob McDonald --- gitk | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gitk b/gitk index 27302749669c21..0f3571050bef1c 100755 --- a/gitk +++ b/gitk @@ -2469,7 +2469,8 @@ proc makewindow {} { -selectbackground $selectbgcolor \ -background $bgcolor -bd 0 \ -xscrollincr $linespc \ - -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll" + -yscrollincr $linespc -yscrollcommand "scrollcanv $cscroll" \ + -xscrollcommand ".tf.histframe.cxsb set" .tf.histframe.pwclist add $canv set canv2 .tf.histframe.pwclist.canv2 canvas $canv2 \ @@ -2487,9 +2488,11 @@ proc makewindow {} { .tf.histframe.pwclist sashpos 0 [lindex $::geometry(pwsash0) 0] } - # a scroll bar to rule them + # a scroll bar to rule them (vertical), and one for horizontal scroll of left pane ttk::scrollbar $cscroll -command {allcanvs yview} pack $cscroll -side right -fill y + ttk::scrollbar .tf.histframe.cxsb -orient horizontal -command "$canv xview" + pack .tf.histframe.cxsb -side bottom -fill x bind .tf.histframe.pwclist {resizeclistpanes %W %w} lappend bglist $canv $canv2 $canv3 pack .tf.histframe.pwclist -fill both -expand 1 -side left From 82fd5de376c39bcb6f5f2a9012ce21bd232276a4 Mon Sep 17 00:00:00 2001 From: basuradeluis Date: Wed, 1 Apr 2026 12:19:01 +0200 Subject: [PATCH 002/436] gitk: spanish translations Signed-off-by: basuradeluis --- po/es.po | 488 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 284 insertions(+), 204 deletions(-) diff --git a/po/es.po b/po/es.po index 25260c6571c1c3..881738e9e6af1f 100644 --- a/po/es.po +++ b/po/es.po @@ -8,30 +8,30 @@ msgid "" msgstr "" "Project-Id-Version: Gitk\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-05-17 14:32+1000\n" -"PO-Revision-Date: 2008-03-25 11:20+0100\n" -"Last-Translator: Santiago Gala \n" +"POT-Creation-Date: 2026-04-18 18:45+0200\n" +"PO-Revision-Date: 2026-04-19 20:18+0200\n" +"Last-Translator: Luis B \n" "Language-Team: Spanish\n" -"Language: \n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.4.2\n" msgid "Couldn't get list of unmerged files:" msgstr "Imposible obtener la lista de archivos pendientes de fusión:" msgid "Color words" -msgstr "" +msgstr "Colorear palabras" msgid "Markup words" -msgstr "" +msgstr "Marcar cambios por palabras" -#, fuzzy msgid "Error parsing revisions:" -msgstr "Error al leer las diferencias de fusión:" +msgstr "Error procesando revisiones:" msgid "Error executing --argscmd command:" -msgstr "" +msgstr "Error ejecutando comando --argscmd:" msgid "No files selected: --merge specified but no files are unmerged." msgstr "" @@ -45,9 +45,8 @@ msgstr "" "No hay archivos seleccionados: se seleccionó la opción --merge pero los " "archivos especificados no necesitan fusión." -#, fuzzy msgid "Error executing git log:" -msgstr "Error al crear la etiqueta:" +msgstr "Error ejecutando git log:" msgid "Reading" msgstr "Leyendo" @@ -74,19 +73,19 @@ msgid "Cancel" msgstr "Cancelar" msgid "&Update" -msgstr "Actualizar" +msgstr "Act&ualizar" msgid "&Reload" -msgstr "" +msgstr "&Recargar" msgid "Reread re&ferences" -msgstr "Releer referencias" +msgstr "Releer re&ferencias" msgid "&List references" -msgstr "Lista de referencias" +msgstr "Mostrar &lista de referencias" msgid "Start git &gui" -msgstr "" +msgstr "Ejecutar 'git &gui'" msgid "&Quit" msgstr "Salir" @@ -124,11 +123,11 @@ msgstr "Combinaciones de teclas" msgid "&Help" msgstr "Ayuda" -msgid "SHA1 ID:" -msgstr "SHA1 ID:" +msgid "Commit ID:" +msgstr "Commit ID:" msgid "Row" -msgstr "" +msgstr "Fila" msgid "Find" msgstr "Buscar" @@ -146,7 +145,7 @@ msgid "adding/removing string:" msgstr "que añade/elimina cadena:" msgid "changing lines matching:" -msgstr "" +msgstr "que cambia líneas que coinciden con:" msgid "Exact" msgstr "Exacto" @@ -170,7 +169,7 @@ msgid "Author" msgstr "Autor" msgid "Committer" -msgstr "" +msgstr "Comiteador" msgid "Search" msgstr "Buscar" @@ -188,10 +187,10 @@ msgid "Lines of context" msgstr "Líneas de contexto" msgid "Ignore space change" -msgstr "Ignora cambios de espaciado" +msgstr "Ignorar cambios de espaciado" msgid "Line diff" -msgstr "" +msgstr "Diferencia de líneas" msgid "Patch" msgstr "Parche" @@ -199,6 +198,9 @@ msgstr "Parche" msgid "Tree" msgstr "Árbol" +msgid "Unknown windowing system, cannot bind mouse" +msgstr "Sistema de ventanas desconocido, no se puede asociar el ratón" + msgid "Diff this -> selected" msgstr "Diferencia de esta -> seleccionada" @@ -206,11 +208,14 @@ msgid "Diff selected -> this" msgstr "Diferencia de seleccionada -> esta" msgid "Make patch" -msgstr "Crear patch" +msgstr "Crear parche" msgid "Create tag" msgstr "Crear etiqueta" +msgid "Copy commit reference" +msgstr "Copiar referencia de la revisión" + msgid "Write commit to file" msgstr "Escribir revisiones a archivo" @@ -223,39 +228,38 @@ msgstr "Añadir esta revisión a la rama actual (cherry-pick)" msgid "Reset HEAD branch to here" msgstr "Traer la rama HEAD aquí" -#, fuzzy msgid "Mark this commit" -msgstr "Añadir esta revisión a la rama actual (cherry-pick)" +msgstr "Marcar esta revisión" msgid "Return to mark" -msgstr "" +msgstr "Volver a revisión marcada" msgid "Find descendant of this and mark" -msgstr "" +msgstr "Encontrar primer descendiente de esta y revisión marcada" msgid "Compare with marked commit" -msgstr "" +msgstr "Comparar con revisión marcada" -#, fuzzy msgid "Diff this -> marked commit" -msgstr "Diferencia de esta -> seleccionada" +msgstr "Diferencia de esta -> revisión marcada" -#, fuzzy msgid "Diff marked commit -> this" -msgstr "Diferencia de seleccionada -> esta" +msgstr "Diferencia de revisión marcada -> esta" -#, fuzzy msgid "Revert this commit" -msgstr "Añadir esta revisión a la rama actual (cherry-pick)" +msgstr "Revertir este commit" msgid "Check out this branch" msgstr "Cambiar a esta rama" +msgid "Rename this branch" +msgstr "Renombrar esta rama" + msgid "Remove this branch" msgstr "Eliminar esta rama" msgid "Copy branch name" -msgstr "" +msgstr "Copiar nombre de rama" msgid "Highlight this too" msgstr "Seleccionar también" @@ -264,21 +268,23 @@ msgid "Highlight this only" msgstr "Seleccionar sólo" msgid "External diff" -msgstr "" +msgstr "Comparador de diferencias externo" msgid "Blame parent commit" -msgstr "" +msgstr "Ejecutar git blame en el commit padre" msgid "Copy path" -msgstr "" +msgstr "Copiar ruta" msgid "Show origin of this line" -msgstr "" +msgstr "Mostrar origen de esta línea" msgid "Run git gui blame on this line" -msgstr "" +msgstr "Ejecutar git gui blame en esta línea" + +msgid "About gitk" +msgstr "Acerca de gitk" -#, fuzzy msgid "" "\n" "Gitk - a commit viewer for git\n" @@ -290,7 +296,7 @@ msgstr "" "\n" "Gitk - un visualizador de revisiones para git\n" "\n" -"Copyright \\u00a9 2005-2016 Paul Mackerras\n" +"Copyright © 2005-2016 Paul Mackerras\n" "\n" "Uso y redistribución permitidos según los términos de la Licencia Pública " "General de GNU (GNU GPL)" @@ -308,9 +314,9 @@ msgstr "Combinaciones de tecla de Gitk:" msgid "<%s-Q>\t\tQuit" msgstr "<%s-Q>\t\tSalir" -#, fuzzy, tcl-format +#, tcl-format msgid "<%s-W>\t\tClose window" -msgstr "<%s-F>\t\tBuscar" +msgstr "<%s-W>\t\tCerrar ventana" msgid "\t\tMove to first commit" msgstr "\t\tIr a la primera revisión" @@ -318,24 +324,21 @@ msgstr "\t\tIr a la primera revisión" msgid "\t\tMove to last commit" msgstr "\t\tIr a la última revisión" -#, fuzzy msgid ", p, k\tMove up one commit" -msgstr ", p, i\tSubir una revisión" +msgstr ", p, k\tSubir una revisión" -#, fuzzy msgid ", n, j\tMove down one commit" -msgstr ", n, k\tBajar una revisión" +msgstr ", n, j\tBajar una revisión" -#, fuzzy msgid ", z, h\tGo back in history list" -msgstr ", z, j\tRetroceder en la historia" +msgstr ", z, h\tRetroceder en la historia" msgid ", x, l\tGo forward in history list" msgstr ", x, l\tAvanzar en la historia" #, tcl-format msgid "<%s-n>\tGo to n-th parent of current commit in history list" -msgstr "" +msgstr "<%s-n>\tIr al padre número N del commit actual" msgid "\tMove up one page in commit list" msgstr "\tSubir una página en la lista de revisiones" @@ -399,12 +402,11 @@ msgstr "<%s-G>\t\tBuscar el siguiente" msgid "\tMove to next find hit" msgstr "\tBuscar el siguiente" -#, fuzzy msgid "g\t\tGo to commit" -msgstr "\t\tIr a la última revisión" +msgstr "g\t\tIr a la revisión" msgid "/\t\tFocus the search box" -msgstr "" +msgstr "/\t\tFoco en el cuadro de búsqueda" msgid "?\t\tMove to previous find hit" msgstr "?\t\tBuscar el anterior" @@ -439,51 +441,54 @@ msgstr "<%s-minus>\tDisminuir tamaño del texto" msgid "\t\tUpdate" msgstr "\t\tActualizar" -#, fuzzy, tcl-format +#, tcl-format msgid "Error creating temporary directory %s:" -msgstr "Error en la creación del parche:" +msgstr "Error creando directorio temporal %s:" -#, fuzzy, tcl-format +#, tcl-format msgid "Error getting \"%s\" from %s:" -msgstr "Error al leer las diferencias de fusión:" +msgstr "Error obteniendo \"%s\" de %s:" -#, fuzzy msgid "command failed:" -msgstr "Línea de comandos" +msgstr "el comando falló:" -#, fuzzy msgid "No such commit" -msgstr "No se han guardado cambios" +msgstr "No existe el commit" msgid "git gui blame: command failed:" -msgstr "" +msgstr "git gui blame: error de ejecución:" #, tcl-format msgid "Couldn't read merge head: %s" -msgstr "" +msgstr "No se pudo leer merge head: %s" -#, fuzzy, tcl-format +#, tcl-format msgid "Error reading index: %s" -msgstr "Error al crear la etiqueta:" +msgstr "Error leyendo índice:%s" #, tcl-format msgid "Couldn't start git blame: %s" -msgstr "" +msgstr "No se pudo iniciar git blame: %s" msgid "Searching" msgstr "Buscando" -#, fuzzy, tcl-format +#, tcl-format msgid "Error running git blame: %s" -msgstr "Error al crear la etiqueta:" +msgstr "Error al ejecutar git blame: %s" #, tcl-format msgid "That line comes from commit %s, which is not in this view" -msgstr "" +msgstr "Esa línea proviene del commit %s, que no está en esta vista" -#, fuzzy msgid "External diff viewer failed:" -msgstr "f\t\tDesplazar la vista de diferencias al archivo siguiente" +msgstr "Visor de diferencias externo falló:" + +msgid "All files" +msgstr "Todos los archivos" + +msgid "View" +msgstr "Vista" msgid "Gitk view definition" msgstr "Definición de vistas de Gitk" @@ -492,96 +497,93 @@ msgid "Remember this view" msgstr "Recordar esta vista" msgid "References (space separated list):" -msgstr "" +msgstr "Referencias (lista separada por espacios):" msgid "Branches & tags:" -msgstr "" +msgstr "Ramas y etiquetas:" -#, fuzzy msgid "All refs" -msgstr "Todos los archivos" +msgstr "Todas las referencias" msgid "All (local) branches" -msgstr "" +msgstr "Todas las ramas (locales)" msgid "All tags" -msgstr "" +msgstr "Todas las etiquetas" msgid "All remote-tracking branches" -msgstr "" +msgstr "Todas las ramas remotas" msgid "Commit Info (regular expressions):" -msgstr "" +msgstr "Información del commit (expresiones regulares)" -#, fuzzy msgid "Author:" -msgstr "Autor" +msgstr "Autor:" -#, fuzzy msgid "Committer:" -msgstr "revisión" +msgstr "Comiteador:" msgid "Commit Message:" -msgstr "" +msgstr "Mensaje del commit:" msgid "Matches all Commit Info criteria" -msgstr "" +msgstr "Cumple todos los criterios de 'Información del commit'" msgid "Matches no Commit Info criteria" -msgstr "" +msgstr "Cumple ningún criterio de 'Información del commit'" msgid "Changes to Files:" -msgstr "" +msgstr "Cambios en ficheros:" msgid "Fixed String" -msgstr "" +msgstr "Texto exacto" msgid "Regular Expression" -msgstr "" +msgstr "Expresión regular" -#, fuzzy msgid "Search string:" -msgstr "Buscando" +msgstr "Buscar cadena:" +# Notice git log since and until format msgid "" "Commit Dates (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, 2009 " "15:27:38\"):" msgstr "" +"Fechas del commit (\"2 weeks ago\", \"2009-03-17 15:27:38\", \"March 17, " +"2009 15:27:38\"):" msgid "Since:" -msgstr "" +msgstr "Desde:" msgid "Until:" -msgstr "" +msgstr "Hasta:" msgid "Limit and/or skip a number of revisions (positive integer):" -msgstr "" +msgstr "Limitar y/o saltar las siguientes revisiones (número positivo)" msgid "Number to show:" -msgstr "" +msgstr "Limitar a n revisiones:" msgid "Number to skip:" -msgstr "" +msgstr "Saltar n revisiones:" msgid "Miscellaneous options:" -msgstr "" +msgstr "Opciones varias:" msgid "Strictly sort by date" -msgstr "" +msgstr "Ordenar estrictamente por fecha" msgid "Mark branch sides" -msgstr "" +msgstr "Marcar lados de ramas" -#, fuzzy msgid "Limit to first parent" -msgstr "Limitar las diferencias a las rutas seleccionadas" +msgstr "Limitar al primer padre" msgid "Simple history" -msgstr "" +msgstr "Historia simple" -#, fuzzy msgid "Additional arguments to git log:" -msgstr "Revisiones a incluir (argumentos a git log):" +msgstr "Argumentos extra a git log:" msgid "Enter files and directories to include, one per line:" msgstr "Introducir archivos y directorios a incluir, uno por línea:" @@ -590,17 +592,16 @@ msgid "Command to generate more commits to include:" msgstr "Comando que genera más revisiones a incluir:" msgid "Gitk: edit view" -msgstr "" +msgstr "Gitk: editar vista" msgid "-- criteria for selecting revisions" -msgstr "" +msgstr "-- criterios para filtrar revisiones" -#, fuzzy msgid "View Name" -msgstr "Vista" +msgstr "Nombre de la vista" msgid "Apply (F5)" -msgstr "" +msgstr "Aplicar (F5)" msgid "Error in commit selection arguments:" msgstr "Error en los argumentos de selección de las revisiones:" @@ -626,11 +627,14 @@ msgstr "Cambios locales añadidos al índice pero sin completar revisión" msgid "Local uncommitted changes, not checked in to index" msgstr "Cambios locales sin añadir al índice" +msgid "Error starting web browser:" +msgstr "Error al arrancar navegador web:" + msgid "and many more" -msgstr "" +msgstr "y muchos más" msgid "many" -msgstr "" +msgstr "muchos" msgid "Tags:" msgstr "Etiquetas:" @@ -650,28 +654,28 @@ msgstr "Sigue-a" msgid "Precedes" msgstr "Precede-a" -#, fuzzy, tcl-format +#, tcl-format msgid "Error getting diffs: %s" -msgstr "Error al leer las diferencias de fusión:" +msgstr "Error al leer las diferencias: %s" msgid "Goto:" msgstr "Ir a:" #, tcl-format -msgid "Short SHA1 id %s is ambiguous" -msgstr "La id SHA1 abreviada %s es ambigua" +msgid "Short commit ID %s is ambiguous" +msgstr "El id SHA1 abreviado %s es ambiguo" -#, fuzzy, tcl-format +#, tcl-format msgid "Revision %s is not known" -msgstr "La id SHA1 %s es desconocida" +msgstr "Revisión %s es desconocida" #, tcl-format -msgid "SHA1 id %s is not known" -msgstr "La id SHA1 %s es desconocida" +msgid "Commit ID %s is not known" +msgstr "El id del commit %s es desconocido" #, tcl-format msgid "Revision %s is not in the current view" -msgstr "" +msgstr "La revisión %s no está en la vista actual" msgid "Date" msgstr "Fecha" @@ -681,51 +685,55 @@ msgstr "Hijas" #, tcl-format msgid "Reset %s branch to here" -msgstr "Poner la rama %s en esta revisión" +msgstr "Resetear la rama %s en esta revisión" msgid "Detached head: can't reset" -msgstr "" +msgstr "En estado 'detached head': no se puede resetear la rama" msgid "Skipping merge commit " -msgstr "" +msgstr "Omitiendo el commit de merge " -#, fuzzy msgid "Error getting patch ID for " -msgstr "Error en la creación del parche:" +msgstr "Error obteniendo ID del parche para " msgid " - stopping\n" -msgstr "" +msgstr " - deteniendo\n" -#, fuzzy msgid "Commit " -msgstr "revisión" +msgstr "Revisión " msgid "" " is the same patch as\n" " " msgstr "" +" es el mismo parche que\n" +" " msgid "" " differs from\n" " " msgstr "" +" difiere de\n" +" " msgid "" "Diff of commits:\n" "\n" msgstr "" +"Diferencia de commits:\n" +"\n" #, tcl-format msgid " has %s children - stopping\n" -msgstr "" +msgstr " tiene %s hijos - parando\n" -#, fuzzy, tcl-format +#, tcl-format msgid "Error writing commit to file: %s" -msgstr "Error al escribir revisión:" +msgstr "Error al escribir revisión al fichero: %s" -#, fuzzy, tcl-format +#, tcl-format msgid "Error diffing commits: %s" -msgstr "Error al escribir revisión:" +msgstr "Error comparando revisiones: %s" msgid "Top" msgstr "Origen" @@ -764,11 +772,10 @@ msgid "Tag name:" msgstr "Nombre de etiqueta:" msgid "Tag message is optional" -msgstr "" +msgstr "El mensaje de la etiqueta es opcional" -#, fuzzy msgid "Tag message:" -msgstr "Nombre de etiqueta:" +msgstr "Mensaje de la etiqueta:" msgid "Create" msgstr "Crear" @@ -792,15 +799,28 @@ msgstr "Escribir" msgid "Error writing commit:" msgstr "Error al escribir revisión:" +msgid "Create branch" +msgstr "Crear nueva rama" + +#, tcl-format +msgid "Rename branch %s" +msgstr "Renombrar rama '%s'" + +msgid "Rename" +msgstr "Renombrar" + msgid "Name:" msgstr "Nombre:" msgid "Please specify a name for the new branch" msgstr "Especifique un nombre para la nueva rama" -#, fuzzy, tcl-format +#, tcl-format msgid "Branch '%s' already exists. Overwrite?" -msgstr "La etiqueta \"%s\" ya existe" +msgstr "La rama '%s' ya existe. ¿Sobreescribir?" + +msgid "Please specify a new name for the branch" +msgstr "Especifique un nuevo nombre para la rama" #, tcl-format msgid "Commit %s is already included in branch %s -- really re-apply it?" @@ -814,33 +834,42 @@ msgid "" "Cherry-pick failed because of local changes to file '%s'.\n" "Please commit, reset or stash your changes and try again." msgstr "" +"El comando cherry-pick falló por los cambios locales al fichero '%s'.\n" +"Por favor, comiteé, reseteé o use stash con sus cambios y pruebe de nuevo" msgid "" "Cherry-pick failed because of merge conflict.\n" "Do you wish to run git citool to resolve it?" msgstr "" +"El comando cherry-pick falló debido a un conflicto de fusión.\n" +"¿Desea ejecutar git citool para resolverlo?" msgid "No changes committed" msgstr "No se han guardado cambios" -#, fuzzy, tcl-format +#, tcl-format msgid "Commit %s is not included in branch %s -- really revert it?" -msgstr "La revisión %s ya está incluida en la rama %s -- ¿Volver a aplicarla?" +msgstr "" +"La revisión %s no está incluida en la rama %s -- ¿seguro que quiere " +"revertirla?" -#, fuzzy msgid "Reverting" -msgstr "Reponiendo" +msgstr "Revirtiendo" #, tcl-format msgid "" "Revert failed because of local changes to the following files:%s Please " "commit, reset or stash your changes and try again." msgstr "" +"El comando revert falló por los cambios locales a estos ficheros:%s Por " +"favor, comitéelos, reseteé o use stash y pruebe otra vez" msgid "" "Revert failed because of merge conflict.\n" " Do you wish to run git citool to resolve it?" msgstr "" +"Revert falló por un conflicto de fusión.\n" +"¿Quiere ejecutar git citool para resolverlo?" msgid "Confirm reset" msgstr "Confirmar git reset" @@ -868,6 +897,10 @@ msgstr "" msgid "Resetting" msgstr "Reponiendo" +#, tcl-format +msgid "A local branch named %s exists already" +msgstr "Ya existe una rama local llamada '%s'" + msgid "Checking out" msgstr "Creando copia de trabajo" @@ -889,6 +922,9 @@ msgstr "Etiquetas y ramas: %s" msgid "Filter" msgstr "Filtro" +msgid "Sort refs by type" +msgstr "Ordenar referencias por tipo" + msgid "" "Error reading commit topology information; branch and preceding/following " "tag information will be incomplete." @@ -902,15 +938,6 @@ msgstr "Etiqueta" msgid "Id" msgstr "Id" -msgid "Gitk font chooser" -msgstr "Selector de tipografías gitk" - -msgid "B" -msgstr "B" - -msgid "I" -msgstr "I" - msgid "Commit list display options" msgstr "Opciones de visualización de la lista de revisiones" @@ -924,12 +951,23 @@ msgstr "Ancho máximo del gráfico (en % del panel)" msgid "Show local changes" msgstr "Mostrar cambios locales" -#, fuzzy -msgid "Auto-select SHA1 (length)" -msgstr "Seleccionar automáticamente SHA1 hash" - msgid "Hide remote refs" -msgstr "" +msgstr "Esconder referencias remotas" + +msgid "Refs to hide (space-separated globs)" +msgstr "Referencias a esconder (globs separados por espacios)" + +msgid "Copy commit ID to clipboard" +msgstr "Copiar id de la revisión al portapapeles" + +msgid "Copy commit ID to X11 selection" +msgstr "Copiar id de la revisión a selección X11" + +msgid "Length of commit ID to copy" +msgstr "Longitud del ID de la revisión al copiar" + +msgid "Wheel scrolling multiplier" +msgstr "Multiplicador desplazamiento rueda del ratón" msgid "Diff display options" msgstr "Opciones de visualización de diferencias" @@ -937,92 +975,108 @@ msgstr "Opciones de visualización de diferencias" msgid "Tab spacing" msgstr "Espaciado de tabulador" -#, fuzzy +msgid "Wrap comment text" +msgstr "Ajustar texto de comentarios" + +msgid "Wrap other text" +msgstr "Ajustar otros textos" + msgid "Display nearby tags/heads" -msgstr "Mostrar etiquetas cercanas" +msgstr "Mostrar etiquetas/referencias cercanas" msgid "Maximum # tags/heads to show" -msgstr "" +msgstr "Máximo número de etiquetas/" msgid "Limit diffs to listed paths" msgstr "Limitar las diferencias a las rutas seleccionadas" msgid "Support per-file encodings" -msgstr "" +msgstr "Soporte para codificaciones por archivo" msgid "External diff tool" -msgstr "" +msgstr "Herramienta de comparación externa" msgid "Choose..." -msgstr "" +msgstr "Escoger..." -#, fuzzy -msgid "General options" -msgstr "Generar parche" +msgid "Web browser" +msgstr "Navegador web" -msgid "Use themed widgets" -msgstr "" +msgid "Themes - change requires restart" +msgstr "Temas - los cambios requieren reinicio" -msgid "(change requires restart)" -msgstr "" +msgid "Theme to use after restart" +msgstr "Tema a usar tras reiniciar" -msgid "(currently unavailable)" -msgstr "" +msgid "Theme definition file" +msgstr "Fichero de definición del tema" -msgid "Colors: press to choose" -msgstr "Colores: pulse para seleccionar" +msgid "The theme definition file may affect all themes." +msgstr "El fichero de definición del tema puede afectar a todos los temas." -msgid "Interface" -msgstr "" +msgid "Apply theme" +msgstr "Aplicar tema" -#, fuzzy -msgid "interface" -msgstr "Tipografía para interfaz de usuario" +msgid "Colors: press to choose" +msgstr "Colores: pulse para seleccionar" msgid "Background" msgstr "Fondo" -#, fuzzy msgid "background" -msgstr "Fondo" +msgstr "fondo" msgid "Foreground" msgstr "Primer plano" -#, fuzzy msgid "foreground" -msgstr "Primer plano" +msgstr "primer plano" msgid "Diff: old lines" -msgstr "Diff: líneas viejas" +msgstr "Diff: líneas antiguas" -#, fuzzy msgid "diff old lines" -msgstr "Diff: líneas viejas" +msgstr "diff líneas antiguas" + +msgid "Diff: old lines bg" +msgstr "Diff: fondo de líneas antiguas" + +msgid "diff old lines bg" +msgstr "diff fondo de líneas antiguas" msgid "Diff: new lines" msgstr "Diff: líneas nuevas" -#, fuzzy msgid "diff new lines" -msgstr "Diff: líneas nuevas" +msgstr "diff líneas nuevas" + +msgid "Diff: new lines bg" +msgstr "Diff: fondo de líneas nuevas" + +msgid "diff new lines bg" +msgstr "diff fondo de líneas nuevas" msgid "Diff: hunk header" msgstr "Diff: cabecera de fragmento" -#, fuzzy msgid "diff hunk header" -msgstr "Diff: cabecera de fragmento" +msgstr "diff cabecera de fragmento" msgid "Marked line bg" -msgstr "" +msgstr "Fondo de línea marcada" msgid "marked line background" -msgstr "" +msgstr "fondo de linea marcada" msgid "Select bg" msgstr "Color de fondo de la selección" +msgid "Link" +msgstr "Enlace" + +msgid "link" +msgstr "enlace" + msgid "Fonts: press to choose" msgstr "Tipografías: pulse para elegir" @@ -1038,27 +1092,22 @@ msgstr "Tipografía para interfaz de usuario" msgid "Gitk preferences" msgstr "Preferencias de gitk" -#, fuzzy msgid "General" -msgstr "Generar" +msgstr "General" msgid "Colors" -msgstr "" +msgstr "Colores" msgid "Fonts" -msgstr "" +msgstr "Fuentes" + +msgid "Gitk: select theme definition" +msgstr "Gitk: escoger definición de tema" #, tcl-format msgid "Gitk: choose color for %s" msgstr "Gitk: elegir color para %s" -msgid "" -"Sorry, gitk cannot run with this version of Tcl/Tk.\n" -" Gitk requires at least Tcl/Tk 8.4." -msgstr "" -"Esta versión de Tcl/Tk es demasiado antigua.\n" -" Gitk requiere Tcl/Tk versión 8.4 o superior." - msgid "Cannot find a git repository here." msgstr "No hay un repositorio git aquí." @@ -1070,6 +1119,37 @@ msgstr "" msgid "Bad arguments to gitk:" msgstr "Argumentos incorrectos a Gitk:" +#~ msgid "SHA1 ID:" +#~ msgstr "SHA1 ID:" + +#~ msgid "Gitk font chooser" +#~ msgstr "Selector de tipografías gitk" + +#~ msgid "B" +#~ msgstr "B" + +#~ msgid "I" +#~ msgstr "I" + +#, fuzzy +#~ msgid "Auto-select SHA1 (length)" +#~ msgstr "Seleccionar automáticamente SHA1 hash" + +#, fuzzy +#~ msgid "General options" +#~ msgstr "Generar parche" + +#, fuzzy +#~ msgid "interface" +#~ msgstr "Tipografía para interfaz de usuario" + +#~ msgid "" +#~ "Sorry, gitk cannot run with this version of Tcl/Tk.\n" +#~ " Gitk requires at least Tcl/Tk 8.4." +#~ msgstr "" +#~ "Esta versión de Tcl/Tk es demasiado antigua.\n" +#~ " Gitk requiere Tcl/Tk versión 8.4 o superior." + #~ msgid "SHA1 ID: " #~ msgstr "SHA1 ID: " From fabae2b99ac11e7d2fcf21b36c86d8080534ddc5 Mon Sep 17 00:00:00 2001 From: Matteo Beniamino Date: Wed, 22 Apr 2026 18:36:04 +0200 Subject: [PATCH 003/436] l10n: it: fix italian usage messages alignment Fixed a misalignment in the "usage:" and " or:" lines in the italian help messages. Signed-off-by: Matteo Beniamino --- po/it.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/it.po b/po/it.po index 20fd8bb28d8589..b5ccd8c7317e31 100644 --- a/po/it.po +++ b/po/it.po @@ -5400,7 +5400,7 @@ msgstr "uso: %s" #: parse-options.c:915 #, c-format msgid " or: %s" -msgstr " oppure: %s" +msgstr " o: %s" #: parse-options.c:918 #, c-format From 4b0a8b2506494323ae5c209ee7df6c08fec3831d Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 21 May 2026 14:06:07 +0000 Subject: [PATCH 004/436] remote: qualify "git pull" advice for non-upstream compareBranches Enable ENABLE_ADVICE_PULL for push-branch comparisons too, not just the upstream entry, so the "use git pull" hint prints when the local branch is behind its push branch. Spell out "git pull " so running the suggested command actually pulls the ref the user was told about; plain "git pull" would fetch the upstream instead. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- remote.c | 48 +++++++++++++++---- t/t6040-tracking-info.sh | 100 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 8 deletions(-) diff --git a/remote.c b/remote.c index a664cd166aa3b9..8870598d44a896 100644 --- a/remote.c +++ b/remote.c @@ -2267,6 +2267,8 @@ static void format_branch_comparison(struct strbuf *sb, bool up_to_date, int ours, int theirs, const char *branch_name, + const char *push_remote_name, + const char *push_branch_name, enum ahead_behind_flags abf, unsigned flags) { @@ -2302,9 +2304,15 @@ static void format_branch_comparison(struct strbuf *sb, "and can be fast-forwarded.\n", theirs), branch_name, theirs); - if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) - strbuf_addstr(sb, - _(" (use \"git pull\" to update your local branch)\n")); + if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) { + if (push_remote_name && push_branch_name) + strbuf_addf(sb, + _(" (use \"git pull %s %s\" to update your local branch)\n"), + push_remote_name, push_branch_name); + else + strbuf_addstr(sb, + _(" (use \"git pull\" to update your local branch)\n")); + } } else { strbuf_addf(sb, Q_("Your branch and '%s' have diverged,\n" @@ -2315,9 +2323,15 @@ static void format_branch_comparison(struct strbuf *sb, "respectively.\n", ours + theirs), branch_name, ours, theirs); - if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) - strbuf_addstr(sb, - _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); + if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) { + if (push_remote_name && push_branch_name) + strbuf_addf(sb, + _(" (use \"git pull %s %s\" if you want to integrate the remote branch with yours)\n"), + push_remote_name, push_branch_name); + else + strbuf_addstr(sb, + _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); + } } } @@ -2355,6 +2369,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, int ours, theirs, cmp; int is_upstream, is_push; unsigned flags = 0; + const char *push_remote_name = NULL; + const char *push_branch_name = NULL; full_ref = resolve_compare_branch(branch, branches.items[i].string); @@ -2398,11 +2414,27 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, if (is_upstream) flags |= ENABLE_ADVICE_PULL; - if (is_push) - flags |= ENABLE_ADVICE_PUSH; if (show_divergence_advice && is_upstream) flags |= ENABLE_ADVICE_DIVERGENCE; + if (is_push) { + flags |= ENABLE_ADVICE_PUSH; + if (!upstream_ref || strcmp(upstream_ref, full_ref)) { + push_remote_name = pushremote_for_branch(branch, NULL); + if (push_remote_name && + skip_prefix(full_ref, "refs/remotes/", &push_branch_name) && + skip_prefix(push_branch_name, push_remote_name, &push_branch_name) && + *push_branch_name == '/') { + push_branch_name++; + flags |= ENABLE_ADVICE_PULL; + } else { + push_remote_name = NULL; + } + } else { + flags |= ENABLE_ADVICE_PULL; + } + } format_branch_comparison(sb, !cmp, ours, theirs, short_ref, + push_remote_name, push_branch_name, abf, flags); reported = 1; diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 0242b5bf7abedb..91cbb8775d0a5a 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -646,4 +646,104 @@ test_expect_success 'status.compareBranches with remapped push and upstream remo test_cmp expect actual ' +test_expect_success 'status.compareBranches behind both upstream and push' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature13 upstream/main && + (cd test && advance work13) && + git -C test push origin && + git -C test branch --set-upstream-to upstream/ahead && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature13 + Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + + Your branch is behind ${SQ}origin/feature13${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin feature13" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with remapped push and behind push branch' ' + test_config -C test remote.pushDefault origin && + test_config -C test remote.origin.push refs/heads/feature14:refs/heads/remapped14 && + test_config -C test status.compareBranches "@{push}" && + git -C test checkout -b feature14 upstream/main && + (cd test && advance work14) && + git -C test push && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature14 + Your branch is behind ${SQ}origin/remapped14${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin remapped14" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with behind push branch and no upstream' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{push}" && + git -C test checkout --no-track -b feature15 upstream/main && + (cd test && advance work15) && + git -C test push origin && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature15 + Your branch is behind ${SQ}origin/feature15${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull origin feature15" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches behind upstream-equals-push suggests plain pull' ' + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature16 origin/main && + (cd test && advance work16) && + git -C test push origin HEAD:main && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature16 + Your branch is behind ${SQ}origin/main${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches suppresses advice when push tracking ref is unconventional' ' + test_config -C test push.default current && + test_config -C test remote.imported.url ../. && + test_config -C test remote.imported.fetch "+refs/heads/*:refs/imported/imported/*" && + test_config -C test branch.feature17.pushRemote imported && + test_config -C test status.compareBranches "@{push}" && + git -C test fetch imported && + git -C test checkout --no-track -b feature17 refs/imported/imported/main && + (cd test && advance work17) && + git -C test push imported HEAD:feature17 && + git -C test fetch imported && + git -C test reset --hard HEAD^ && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature17 + Your branch is behind ${SQ}imported/imported/feature17${SQ} by 1 commit, and can be fast-forwarded. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + test_done From 4ed1ffe680d1ad0fe7436c9816262b6abb518629 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:13 +0200 Subject: [PATCH 005/436] t5710: simplify 'mkdir X' followed by 'git -C X init' It's simpler and more efficient to just use `git init client` instead of `mkdir client && git -C client init`. So let's replace the latter with the former. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- t/t5710-promisor-remote-capability.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index b404ad9f0a9e3d..bf1cc54605394e 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -177,8 +177,7 @@ test_expect_success "init + fetch with promisor.advertise set to 'true'" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && - mkdir client && - git -C client init && + git init client && git -C client config remote.lop.promisor true && git -C client config remote.lop.fetch "+refs/heads/*:refs/remotes/lop/*" && git -C client config remote.lop.url "$TRASH_DIRECTORY_URL/lop" && @@ -231,8 +230,7 @@ test_expect_success "init + fetch two promisors but only one advertised" ' # Create a promisor that will be configured but not be used git init --bare unused_lop && - mkdir client && - git -C client init && + git init client && git -C client config remote.unused_lop.promisor true && git -C client config remote.unused_lop.fetch "+refs/heads/*:refs/remotes/unused_lop/*" && git -C client config remote.unused_lop.url "$TRASH_DIRECTORY_URL/unused_lop" && From ee7ea4907ccef604f764df5e223640ad04192f6d Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:14 +0200 Subject: [PATCH 006/436] urlmatch: change 'allow_globs' arg to bool The last argument of url_normalize_1() is `char allow_globs` but it is used as a boolean, not as a char. Let's convert it to a `bool`, and while at it convert the two calls to url_normalize_1() so they pass 'true' or 'false' instead of '1' or '0'. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- urlmatch.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/urlmatch.c b/urlmatch.c index bf8cce6de9d8da..b2d88a5289df6c 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -112,7 +112,7 @@ static int match_host(const struct url_info *url_info, return (!url_len && !pat_len); } -static char *url_normalize_1(const char *url, struct url_info *out_info, char allow_globs) +static char *url_normalize_1(const char *url, struct url_info *out_info, bool allow_globs) { /* * Normalize NUL-terminated url using the following rules: @@ -438,7 +438,7 @@ static char *url_normalize_1(const char *url, struct url_info *out_info, char al char *url_normalize(const char *url, struct url_info *out_info) { - return url_normalize_1(url, out_info, 0); + return url_normalize_1(url, out_info, false); } char *url_parse(const char *url_orig, struct url_info *out_info) @@ -704,7 +704,7 @@ int urlmatch_config_entry(const char *var, const char *value, struct url_info norm_info; config_url = xmemdupz(key, dot - key); - norm_url = url_normalize_1(config_url, &norm_info, 1); + norm_url = url_normalize_1(config_url, &norm_info, true); if (norm_url) retval = match_urls(url, &norm_info, &matched); else if (collect->fallback_match_fn) From 58880c82feb460015153575dc02b9959e4d8a8a0 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:15 +0200 Subject: [PATCH 007/436] urlmatch: add url_normalize_pattern() helper In a following commit, we will need to normalize a URL glob pattern (which may contain '*' in the host portion) and extract its component offsets (host, path, etc.) for separate matching. Let's export a dedicated helper function url_normalize_pattern() for that purpose. It works like url_normalize(), but passes allow_globs=true to the internal url_normalize_1(), so that '*' characters in the host are accepted rather than rejected. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- urlmatch.c | 5 +++++ urlmatch.h | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/urlmatch.c b/urlmatch.c index b2d88a5289df6c..20bc2d009cd0dd 100644 --- a/urlmatch.c +++ b/urlmatch.c @@ -441,6 +441,11 @@ char *url_normalize(const char *url, struct url_info *out_info) return url_normalize_1(url, out_info, false); } +char *url_normalize_pattern(const char *url, struct url_info *out_info) +{ + return url_normalize_1(url, out_info, true); +} + char *url_parse(const char *url_orig, struct url_info *out_info) { struct strbuf url; diff --git a/urlmatch.h b/urlmatch.h index 6b3ce428582da3..db1a335e728ea3 100644 --- a/urlmatch.h +++ b/urlmatch.h @@ -37,6 +37,18 @@ struct url_info { char *url_normalize(const char *, struct url_info *); char *url_parse(const char *, struct url_info *); +/* + * Like url_normalize(), but also allows '*' glob characters in the host + * portion. Use this when normalizing URL patterns from user configuration. + * + * Note that '*' is a valid path character per RFC 3986 (as a sub-delim), + * so glob patterns using '*' in the path are also accepted. + * + * Returns a newly allocated normalized string and fills out_info if + * non-NULL, or NULL if the pattern is invalid. + */ +char *url_normalize_pattern(const char *url, struct url_info *out_info); + struct urlmatch_item { size_t hostmatch_len; size_t pathmatch_len; From 53951298515ad26728175182c9103eea71885220 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:16 +0200 Subject: [PATCH 008/436] promisor-remote: add 'local_name' to 'struct promisor_info' In a following commit, we will store promisor remote information under a remote name different than the one the server advertised. To prepare for this change, let's add a new 'char *local_name' member to 'struct promisor_info', and let's update the related functions. While at it, let's also add a small promisor_info_local_name() helper that returns `local_name` when set, `name` otherwise, and let's use this small helper in promisor_store_advertised_fields() and in the post-loop of filter_promisor_remote() so that lookups against the local repo configuration use the right name. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- promisor-remote.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/promisor-remote.c b/promisor-remote.c index 38fa05054227f6..138a41289350d6 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -434,13 +434,14 @@ static struct string_list *fields_stored(void) * Struct for promisor remotes involved in the "promisor-remote" * protocol capability. * - * Except for "name", each in this struct and its - * should correspond (either on the client side or on the server side) - * to a "remote.." config variable set to where - * "" is a promisor remote name. + * Except for "name" and "local_name", each in this struct + * and its should correspond (either on the client side or on + * the server side) to a "remote.." config variable set + * to where "" is a promisor remote name. */ struct promisor_info { - const char *name; + const char *name; /* name the server advertised */ + const char *local_name; /* name used locally (may be auto-generated) */ const char *url; const char *filter; const char *token; @@ -449,6 +450,7 @@ struct promisor_info { static void promisor_info_free(struct promisor_info *p) { free((char *)p->name); + free((char *)p->local_name); free((char *)p->url); free((char *)p->filter); free((char *)p->token); @@ -462,6 +464,11 @@ static void promisor_info_list_clear(struct string_list *list) string_list_clear(list, 0); } +static const char *promisor_info_local_name(struct promisor_info *p) +{ + return p->local_name ? p->local_name : p->name; +} + static void set_one_field(struct promisor_info *p, const char *field, const char *value) { @@ -829,7 +836,7 @@ static bool promisor_store_advertised_fields(struct promisor_info *advertised, { struct promisor_info *p; struct string_list_item *item; - const char *remote_name = advertised->name; + const char *remote_name = promisor_info_local_name(advertised); bool reload_config = false; if (!(store_info->store_filter || store_info->store_token)) @@ -937,7 +944,8 @@ static void filter_promisor_remote(struct repository *repo, /* Apply accepted remotes to the stable repo state */ for_each_string_list_item(item, accepted_remotes) { struct promisor_info *info = item->util; - struct promisor_remote *r = repo_promisor_remote_find(repo, info->name); + const char *remote_name = promisor_info_local_name(info); + struct promisor_remote *r = repo_promisor_remote_find(repo, remote_name); if (r) { r->accepted = 1; From 78e0d9b0e4649659cc38545450174d293cb1ec0c Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:17 +0200 Subject: [PATCH 009/436] promisor-remote: introduce promisor.acceptFromServerUrl The "promisor-remote" protocol capability allows servers to advertise promisor remotes, but doesn't allow these remotes to be automatically configured on the client. Let's introduce a new `promisor.acceptFromServerUrl` config variable which contains a glob pattern, so that advertised remotes with a URL matching that pattern will be automatically configured. The glob pattern can optionally be prefixed with a remote name which will be used as the name of the new local remote. For now though, let's only introduce the functions to read and validate the glob patterns and the optional prefixes. Checking if the URLs of the advertised remotes match the glob patterns and taking the appropriate action is left for a following commit. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- promisor-remote.c | 90 +++++++++++++++++++++++++++ t/t5710-promisor-remote-capability.sh | 21 +++++++ 2 files changed, 111 insertions(+) diff --git a/promisor-remote.c b/promisor-remote.c index 138a41289350d6..8d4f6e0a72251b 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -12,6 +12,7 @@ #include "packfile.h" #include "environment.h" #include "url.h" +#include "urlmatch.h" #include "version.h" struct promisor_remote_config { @@ -657,6 +658,90 @@ static bool has_control_char(const char *s) return false; } +struct allowed_url { + char *remote_name; + char *url_pattern; + struct url_info pattern_info; +}; + +static void allowed_url_free(void *util, const char *str UNUSED) +{ + struct allowed_url *allowed = util; + + if (!allowed) + return; + + /* Depending on prefix, free either remote_name or url_pattern */ + free(allowed->remote_name ? allowed->remote_name : allowed->url_pattern); + free(allowed->pattern_info.url); + free(allowed); +} + +static struct allowed_url *valid_accept_url(const char *url) +{ + char *dup, *p; + struct allowed_url *allowed; + + if (!url) + return NULL; + + dup = xstrdup(url); + p = strchr(dup, '='); + if (p) { + *p = '\0'; + if (!valid_remote_name(dup)) { + warning(_("invalid remote name '%s' before '=' sign " + "in '%s' from promisor.acceptFromServerUrl config"), + dup, url); + free(dup); + return NULL; + } + p++; + } else { + p = dup; + } + + if (has_control_char(p)) { + warning(_("invalid url pattern '%s' " + "in '%s' from promisor.acceptFromServerUrl config"), p, url); + free(dup); + return NULL; + } + + allowed = xmalloc(sizeof(*allowed)); + allowed->remote_name = (p == dup) ? NULL : dup; + allowed->url_pattern = p; + allowed->pattern_info.url = url_normalize_pattern(p, &allowed->pattern_info); + if (!allowed->pattern_info.url) { + warning(_("invalid url pattern '%s' " + "in '%s' from promisor.acceptFromServerUrl config"), p, url); + free(dup); + free(allowed); + return NULL; + } + + return allowed; +} + +static void load_accept_from_server_url(struct repository *repo, + struct string_list *accept_urls) +{ + const struct string_list *config_urls; + + if (!repo_config_get_string_multi(repo, "promisor.acceptfromserverurl", &config_urls)) { + struct string_list_item *item; + + for_each_string_list_item(item, config_urls) { + struct allowed_url *allowed = valid_accept_url(item->string); + if (allowed) { + struct string_list_item *new; + new = string_list_append(accept_urls, item->string); + new->util = allowed; + } + } + } +} + static int should_accept_remote(enum accept_promisor accept, struct promisor_info *advertised, struct string_list *config_info) @@ -901,6 +986,10 @@ static void filter_promisor_remote(struct repository *repo, struct string_list_item *item; bool reload_config = false; enum accept_promisor accept = accept_from_server(repo); + struct string_list accept_urls = STRING_LIST_INIT_DUP; + + /* Load and validate the acceptFromServerUrl config */ + load_accept_from_server_url(repo, &accept_urls); if (accept == ACCEPT_NONE) return; @@ -934,6 +1023,7 @@ static void filter_promisor_remote(struct repository *repo, } } + string_list_clear_func(&accept_urls, allowed_url_free); promisor_info_list_clear(&config_info); string_list_clear(&remote_info, 0); store_info_free(store_info); diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index bf1cc54605394e..3b39505380caa7 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -387,6 +387,27 @@ test_expect_success "clone with 'KnownUrl' and empty url, so not advertised" ' check_missing_objects server 1 "$oid" ' +test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + # As "bad name" contains a space, which is not a valid remote name, + # the pattern should be rejected with a warning and no remote created. + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c "promisor.acceptFromServerUrl=bad name=https://example.com/*" \ + --no-local --filter="blob:limit=5k" server client 2>err && + + # Check that a warning was emitted + test_grep "invalid remote name '\''bad name'\''" err && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + test_expect_success "clone with promisor.sendFields" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && From 5dd8043581ca331dcb59ab721aefb5881128e124 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:18 +0200 Subject: [PATCH 010/436] promisor-remote: trust known remotes matching acceptFromServerUrl A previous commit introduced the `promisor.acceptFromServerUrl` config variable along with the machinery to parse and validate the URL glob patterns and optional remote name prefixes it contains. However, these URL patterns are not yet tied into the client's acceptance logic. When a promisor remote is already configured locally, its fields (like authentication tokens) may occasionally need to be refreshed by the server. If `promisor.acceptFromServer` is set to the secure default ("None"), these updates are rejected, potentially causing future fetches to fail. To enable such targeted updates for trusted URLs, let's use the URL patterns from `promisor.acceptFromServerUrl` as an additional URL based allowlist. Concretely, let's check the advertised URLs against the URL glob patterns by introducing a new small helper function called url_matches_accept_list(), which iterates over the glob patterns and returns the first matching allowed_url entry (or NULL). The URL matching is done component by component: scheme and port are compared exactly, the host and path are matched with wildmatch(). Before matching, the advertised URL is passed through url_normalize() so that case variations in the scheme/host, percent-encoding tricks, and ".." path segments cannot bypass the allowlist. The username and password components of the URL are intentionally ignored during matching to allow servers to rotate them, though using the 'token' field of the capability is preferred over embedding credentials in the URL. Let's then use this helper in should_accept_remote() so that a known remote whose URL matches the allowlist is accepted. To prepare for this new logic, let's also: - Add an 'accept_urls' parameter to should_accept_remote(). - Replace the BUG() guard in the ACCEPT_KNOWN_URL case with an explicit 'if (accept == ACCEPT_KNOWN_URL) return' and a new BUG() guard in the ACCEPT_NONE case. - Call accept_from_server_url() from filter_promisor_remote() and relax its early return so that the function is entered when `accept_urls` has entries even if `accept == ACCEPT_NONE`. With this, many organizations may only need something like: git config set --global \ promisor.acceptFromServerUrl "https://my-org.com/*" to accept only their own remotes. And if they need to accept additional remotes in some specific repos, they can also set: git config set promisor.acceptFromServer knownUrl and configure the additional remote manually only in the repos where they are needed. Let's then properly document `promisor.acceptFromServerUrl` in "promisor.adoc" as an additive security allowlist for known remotes, including the URL normalization behavior and the component-wise matching, and let's mention it in "gitprotocol-v2.adoc". Also let's clarify in the documentation how `promisor.acceptFromServerUrl` interacts with `promisor.acceptFromServer`: - Precedence: when both options are set, `promisor.acceptFromServerUrl` is consulted first. If a matching pattern leads to acceptance, the remote is accepted regardless of `promisor.acceptFromServer`. Otherwise the decision is left to `promisor.acceptFromServer`. - URL-mismatch guard: even when the advertised URL matches the allowlist, an already-existing client-side remote whose configured URL differs from the advertised one is not accepted through `promisor.acceptFromServerUrl`. `promisor.acceptFromServer=all` and `=knownName` keep their pre-existing, looser semantics. The precedence paragraph is intentionally scoped here to known remotes only (field updates). A following commit that introduces auto-creation of unknown remotes will extend it to cover that case as well. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 76 +++++++++++++++++++ Documentation/gitprotocol-v2.adoc | 9 ++- promisor-remote.c | 102 +++++++++++++++++++++++--- t/t5710-promisor-remote-capability.sh | 71 ++++++++++++++++++ 4 files changed, 244 insertions(+), 14 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index b0fa43b8393a53..605473c82f0409 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -51,6 +51,82 @@ promisor.acceptFromServer:: to "fetch" and "clone" requests from the client. Name and URL comparisons are case sensitive. See linkgit:gitprotocol-v2[5]. +promisor.acceptFromServerUrl:: + A glob pattern to specify which server-advertised URLs a + client is allowed to act on. When a URL matches, the client + will accept the advertised remote as a promisor remote and may + automatically accept field updates (such as authentication + tokens) from the server, even if `promisor.acceptFromServer` + is set to `none` (the default). ++ +This option can appear multiple times in config files. An advertised +URL will be accepted if it matches _ANY_ glob pattern specified by +this option in _ANY_ config file read by Git. ++ +When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl` +are set, `promisor.acceptFromServerUrl` is consulted first and takes +precedence: if a matching pattern leads to acceptance (by accepting +field updates for a known remote whose URL matches both the local +configuration and the allowlist), the advertised remote is accepted +regardless of the `promisor.acceptFromServer` setting. If no pattern +in `promisor.acceptFromServerUrl` triggers acceptance, the decision +is left to `promisor.acceptFromServer`. ++ +Note however that, even when an advertised URL matches a pattern in +`promisor.acceptFromServerUrl`, an already-existing remote on the +client whose name matches the advertised name but whose configured URL +differs from the advertised one will _NOT_ be accepted through +`promisor.acceptFromServerUrl`. This prevents a server from silently +re-pointing an existing client-side remote at a different URL. (Such a +remote may still be accepted through `promisor.acceptFromServer=all` +or `=knownName`, which have their own, looser semantics; see the +documentation of that option.) ++ +Be _VERY_ careful with these patterns: `*` matches any sequence of +characters within the 'host' and 'path' parts of a URL (but cannot +cross part boundaries). An overly broad pattern is a major security +risk, as a matching URL allows a server to update fields (such as +authentication tokens) on known remotes without further confirmation. +To minimize security risks, follow these guidelines: ++ +-- +1. Start with a secure protocol scheme, like `https://` or `ssh://`. ++ +2. Only allow domain names or paths where you control and trust _ALL_ + the content. Be especially careful with shared hosting platforms + like `github.com` or `gitlab.com`. A broad pattern like + `https://gitlab.com/*` is dangerous because it trusts every + repository on the entire platform. Always restrict such patterns to + your specific organization or namespace (e.g., + `https://gitlab.com/your-org/*`). ++ +3. Never use globs at the end of domain names. For example, + `https://cdn.your-org.com/*` might be safe, but + `https://cdn.your-org.com*/*` is a major security risk because + the latter matches `https://cdn.your-org.com.hacker.net/repo`. ++ +4. Be careful using globs at the beginning of domain names. While the + code ensures a `*` in the host cannot cross into the path, a + pattern like `https://*.example.com/*` will still match any + subdomain. This is extremely dangerous on shared hosting platforms + (e.g., `https://*.github.io/*` trusts every user's site on the + entire platform). +-- ++ +Before matching, both the advertised URL and the pattern are +normalized: the scheme and host are lowercased, percent-encoded +characters are decoded where possible, and path segments like `..` +are resolved. The port must also match exactly (e.g., +`https://example.com:8080/*` will not match a URL advertised on +port 9999). The username and password components of the URL are +ignored during matching. Note that embedding credentials in URLs is +discouraged. Passing authentication tokens via the `token` field of +the `promisor-remote` capability is strongly preferred. ++ +For the security implications of accepting a promisor remote, see the +documentation of `promisor.acceptFromServer`. For details on the +protocol, see linkgit:gitprotocol-v2[5]. + promisor.checkFields:: A comma or space separated list of additional remote related field names. A client checks if the values of these fields diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc index befa697d21c281..2beb70595fc1e5 100644 --- a/Documentation/gitprotocol-v2.adoc +++ b/Documentation/gitprotocol-v2.adoc @@ -866,10 +866,11 @@ the server advertised, the client shouldn't advertise the On the server side, the "promisor.advertise" and "promisor.sendFields" configuration options can be used to control what it advertises. On -the client side, the "promisor.acceptFromServer" configuration option -can be used to control what it accepts, and the "promisor.storeFields" -option, to control what it stores. See the documentation of these -configuration options in linkgit:git-config[1] for more information. +the client side, the "promisor.acceptFromServer" and +"promisor.acceptFromServerUrl" configuration options can be used to +control what it accepts, and the "promisor.storeFields" option, to +control what it stores. See the documentation of these configuration +options in linkgit:git-config[1] for more information. Note that in the future it would be nice if the "promisor-remote" protocol capability could be used by the server, when responding to diff --git a/promisor-remote.c b/promisor-remote.c index 8d4f6e0a72251b..04a5bb9939928c 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -14,6 +14,7 @@ #include "url.h" #include "urlmatch.h" #include "version.h" +#include "wildmatch.h" struct promisor_remote_config { struct promisor_remote *promisors; @@ -742,8 +743,79 @@ static void load_accept_from_server_url(struct repository *repo, } } +static bool match_pattern_url(const char *pat, size_t pat_len, + const char *url, size_t url_len) +{ + char *p_str = xstrndup(pat, pat_len); + char *u_str = xstrndup(url, url_len); + bool res = !wildmatch(p_str, u_str, 0); + + free(p_str); + free(u_str); + + return res; +} + +static bool match_one_url(const struct url_info *pi, const struct url_info *ui) +{ + const char *pat = pi->url; + const char *url = ui->url; + + /* + * Schemes must match exactly. They are case-folded by + * url_normalize(), so strncmp() suffices. + */ + if (pi->scheme_len != ui->scheme_len || strncmp(pat, url, pi->scheme_len)) + return false; + + /* + * Ports must match exactly. url_normalize() strips default + * ports (like 443 for https), so length and content + * comparisons are sufficient. + */ + if (pi->port_len != ui->port_len || + strncmp(pat + pi->port_off, url + ui->port_off, pi->port_len)) + return false; + + /* + * Match host and path separately to prevent a '*' in the host + * portion of the pattern from matching across the '/' + * boundary into the path. + */ + + return match_pattern_url(pat + pi->host_off, pi->host_len, + url + ui->host_off, ui->host_len) && + match_pattern_url(pat + pi->path_off, pi->path_len, + url + ui->path_off, ui->path_len); +} + +static struct allowed_url *url_matches_accept_list( + struct string_list *accept_urls, const char *url) +{ + struct string_list_item *item; + struct url_info url_info; + + url_info.url = url_normalize(url, &url_info); + + if (!url_info.url) + return NULL; + + for_each_string_list_item(item, accept_urls) { + struct allowed_url *allowed = item->util; + + if (match_one_url(&allowed->pattern_info, &url_info)) { + free(url_info.url); + return allowed; + } + } + + free(url_info.url); + return NULL; +} + static int should_accept_remote(enum accept_promisor accept, struct promisor_info *advertised, + struct string_list *accept_urls, struct string_list *config_info) { struct promisor_info *p; @@ -756,23 +828,27 @@ static int should_accept_remote(enum accept_promisor accept, "this remote should have been rejected earlier", remote_name); - if (accept == ACCEPT_ALL) - return all_fields_match(advertised, config_info, NULL); - /* Get config info for that promisor remote */ item = string_list_lookup(config_info, remote_name); - if (!item) + if (!item) { /* We don't know about that remote */ + if (accept == ACCEPT_ALL) + return all_fields_match(advertised, config_info, NULL); return 0; + } p = item->util; - if (accept == ACCEPT_KNOWN_NAME) + /* Known remote in the allowlist? */ + if (!strcmp(p->url, remote_url) && url_matches_accept_list(accept_urls, remote_url)) return all_fields_match(advertised, config_info, p); - if (accept != ACCEPT_KNOWN_URL) - BUG("Unhandled 'enum accept_promisor' value '%d'", accept); + if (accept == ACCEPT_ALL) + return all_fields_match(advertised, config_info, NULL); + + if (accept == ACCEPT_KNOWN_NAME) + return all_fields_match(advertised, config_info, p); if (strcmp(p->url, remote_url)) { warning(_("known remote named '%s' but with URL '%s' instead of '%s', " @@ -781,7 +857,13 @@ static int should_accept_remote(enum accept_promisor accept, return 0; } - return all_fields_match(advertised, config_info, p); + if (accept == ACCEPT_KNOWN_URL) + return all_fields_match(advertised, config_info, p); + + if (accept != ACCEPT_NONE) + BUG("Unhandled 'enum accept_promisor' value '%d'", accept); + + return 0; } static int skip_field_name_prefix(const char *elem, const char *field_name, const char **value) @@ -991,7 +1073,7 @@ static void filter_promisor_remote(struct repository *repo, /* Load and validate the acceptFromServerUrl config */ load_accept_from_server_url(repo, &accept_urls); - if (accept == ACCEPT_NONE) + if (accept == ACCEPT_NONE && !accept_urls.nr) return; /* Parse remote info received */ @@ -1011,7 +1093,7 @@ static void filter_promisor_remote(struct repository *repo, string_list_sort(&config_info); } - if (should_accept_remote(accept, advertised, &config_info)) { + if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) { if (!store_info) store_info = store_info_new(repo); if (promisor_store_advertised_fields(advertised, store_info)) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index 3b39505380caa7..0659b2ac157f3b 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -387,6 +387,77 @@ test_expect_success "clone with 'KnownUrl' and empty url, so not advertised" ' check_missing_objects server 1 "$oid" ' +test_expect_success "clone with 'None' but URL allowlisted" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with 'None' but URL not in allowlist" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="https://example.com/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + +test_expect_success "clone with 'None' but URL allowlisted in one pattern out of two" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="https://example.com/*" \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with 'None', URL allowlisted, but client has different URL" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + # The client configures "lop" with a different URL (serverTwo) than + # what the server advertises (lop). Even though the advertised URL + # matches the allowlist, the remote is rejected because the + # configured URL does not match the advertised one. + GIT_NO_LAZY_FETCH=0 git clone -c remote.lop.promisor=true \ + -c remote.lop.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.lop.url="$TRASH_DIRECTORY_URL/serverTwo" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the largest object is not missing on the server + check_missing_objects server 0 "" && + + # Reinitialize server so that the largest object is missing again + initialize_server 1 "$oid" +' + test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && From 7a56394fc6c28572925b00c4fe3b1ff78b5f4322 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:19 +0200 Subject: [PATCH 011/436] promisor-remote: auto-configure unknown remotes Previous commits have introduced the `promisor.acceptFromServerUrl` config variable to allowlist some URLs advertised by a server through the "promisor-remote" protocol capability. However the new `promisor.acceptFromServerUrl` mechanism, like the old `promisor.acceptFromServer` mechanism, still requires a remote to already exist in the client's local configuration before it can be accepted. This places a significant manual burden on users to pre-configure these remotes, and creates friction for administrators who have to troubleshoot or manually provision these setups for their teams. To eliminate this burden, let's automatically create a new `[remote]` section in the client's config when a server advertises an unknown remote whose URL matches a `promisor.acceptFromServerUrl` glob pattern. Concretely, let's add four helpers: - sanitize_remote_name(): turn an arbitrary URL-derived string into a valid remote name by replacing non-alphanumeric characters, collapsing runs of '-', and prepending "promisor-auto-". - promisor_remote_name_from_url(): normalize the URL and extract host+port+path to build a human-readable base name, then pass it through sanitize_remote_name(). - configure_auto_promisor_remote(): write the remote.*.url, remote.*.promisor and remote.*.advertisedAs keys to the repo config. - handle_matching_allowed_url(): pick the final name (user-supplied alias or auto-generated), handle collisions by appending "-1", "-2", etc., then call configure_auto_promisor_remote(). Let's also add should_accept_new_remote_url() which reuses the url_matches_accept_list() helper introduced in a previous commit to find a matching pattern, then delegates to handle_matching_allowed_url() to create the remote. And then let's call should_accept_new_remote_url() from the '!item' (unknown remote) branch of should_accept_remote(), setting `reload_config` so that the newly-written config is picked up. Finally let's document all that by: - expanding the `promisor.acceptFromServerUrl` entry to describe auto-creation, the optional "name=" prefix syntax, the "promisor-auto-*" generation rules, and numeric-suffix collision handling, and by - adding a "remote..advertisedAs" entry to "remote.adoc". Also let's extend the precedence paragraph added by a previous commit to mention this new acceptance path: until now, the only way for `promisor.acceptFromServerUrl` to trigger acceptance was to allow field updates for a known remote. With this commit, it can also trigger auto-creation of a previously-unknown remote whose advertised URL matches the allowlist. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 39 +++-- Documentation/config/remote.adoc | 9 ++ promisor-remote.c | 201 +++++++++++++++++++++++++- t/t5710-promisor-remote-capability.sh | 104 +++++++++++++ 4 files changed, 340 insertions(+), 13 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index 605473c82f0409..455ce40be892de 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -54,7 +54,8 @@ promisor.acceptFromServer:: promisor.acceptFromServerUrl:: A glob pattern to specify which server-advertised URLs a client is allowed to act on. When a URL matches, the client - will accept the advertised remote as a promisor remote and may + will accept the advertised remote as a promisor remote, may + automatically create a new remote configuration for it and may automatically accept field updates (such as authentication tokens) from the server, even if `promisor.acceptFromServer` is set to `none` (the default). @@ -65,12 +66,13 @@ this option in _ANY_ config file read by Git. + When both `promisor.acceptFromServer` and `promisor.acceptFromServerUrl` are set, `promisor.acceptFromServerUrl` is consulted first and takes -precedence: if a matching pattern leads to acceptance (by accepting -field updates for a known remote whose URL matches both the local -configuration and the allowlist), the advertised remote is accepted -regardless of the `promisor.acceptFromServer` setting. If no pattern -in `promisor.acceptFromServerUrl` triggers acceptance, the decision -is left to `promisor.acceptFromServer`. +precedence: if a matching pattern leads to acceptance (either by +auto-configuring an unknown remote or by accepting field updates for +a known remote whose URL matches both the local configuration and the +allowlist), the advertised remote is accepted regardless of the +`promisor.acceptFromServer` setting. If no pattern in +`promisor.acceptFromServerUrl` triggers acceptance, the decision is +left to `promisor.acceptFromServer`. + Note however that, even when an advertised URL matches a pattern in `promisor.acceptFromServerUrl`, an already-existing remote on the @@ -85,9 +87,10 @@ documentation of that option.) Be _VERY_ careful with these patterns: `*` matches any sequence of characters within the 'host' and 'path' parts of a URL (but cannot cross part boundaries). An overly broad pattern is a major security -risk, as a matching URL allows a server to update fields (such as -authentication tokens) on known remotes without further confirmation. -To minimize security risks, follow these guidelines: +risk, as a matching URL allows a server to auto-configure new remotes +and to update fields (such as authentication tokens) on known remotes +without further confirmation. To minimize security risks, follow these +guidelines: + -- 1. Start with a secure protocol scheme, like `https://` or `ssh://`. @@ -123,6 +126,22 @@ ignored during matching. Note that embedding credentials in URLs is discouraged. Passing authentication tokens via the `token` field of the `promisor-remote` capability is strongly preferred. + +The glob pattern can optionally be prefixed with a remote name and an +equals sign (e.g., `cdn=https://cdn.example.com/*`). If such a prefix +is provided, accepted remotes will be saved under that name. If no +such prefix is provided, a safe remote name will be automatically +generated by sanitizing the URL and prefixing it with +`promisor-auto-`. ++ +If a remote with the chosen name already exists but points to a +different URL, Git will append a numeric suffix (e.g., `-1`, `-2`) to +the name to prevent overwriting existing configurations. You should +make sure that this doesn't happen often though, as remotes will be +rejected if the numeric suffix increases too much. In all cases, the +original name advertised by the server is recorded in the +`remote..advertisedAs` configuration variable for tracing and +debugging purposes. ++ For the security implications of accepting a promisor remote, see the documentation of `promisor.acceptFromServer`. For details on the protocol, see linkgit:gitprotocol-v2[5]. diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 91e46f66f5dd1c..6e2bbdf4572db3 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -91,6 +91,15 @@ remote..promisor:: When set to true, this remote will be used to fetch promisor objects. +remote..advertisedAs:: + When a promisor remote is automatically configured using + information advertised by a server through the + `promisor-remote` protocol capability (see + `promisor.acceptFromServerUrl`), the server's originally + advertised name is saved in this variable. This is for + information, tracing and debugging purposes. Users should not + typically modify or create such configuration entries. + remote..partialclonefilter:: The filter that will be applied when fetching from this promisor remote. Changing or clearing this value will only affect fetches for new commits. diff --git a/promisor-remote.c b/promisor-remote.c index 04a5bb9939928c..8fb5e40f670207 100644 --- a/promisor-remote.c +++ b/promisor-remote.c @@ -813,10 +813,197 @@ static struct allowed_url *url_matches_accept_list( return NULL; } -static int should_accept_remote(enum accept_promisor accept, +/* + * Sanitize the buffer to make it a valid remote name coming from the + * server by: + * + * - replacing any non alphanumeric character with a '-' + * - stripping any leading '-', + * - condensing multiple '-' into one, + * - prepending "promisor-auto-", + * - validating the result. + */ +static int sanitize_remote_name(struct strbuf *buf, const char *url) +{ + char prev = '-'; + for (size_t i = 0; i < buf->len; ) { + if (!isalnum(buf->buf[i])) + buf->buf[i] = '-'; + if (prev == '-' && buf->buf[i] == '-') { + strbuf_remove(buf, i, 1); + } else { + prev = buf->buf[i]; + i++; + } + } + + strbuf_strip_suffix(buf, "-"); + + if (!buf->len) { + warning(_("couldn't generate a valid remote name from " + "advertised url '%s', ignoring this remote"), url); + return -1; + } + + strbuf_insertstr(buf, 0, "promisor-auto-"); + + if (!valid_remote_name(buf->buf)) { + warning(_("generated remote name '%s' from advertised url '%s' " + "is invalid, ignoring this remote"), buf->buf, url); + return -1; + } + + return 0; +} + +static char *promisor_remote_name_from_url(const char *url) +{ + struct url_info url_info = { 0 }; + char *normalized = url_normalize(url, &url_info); + struct strbuf buf = STRBUF_INIT; + + if (!normalized) { + warning(_("couldn't normalize advertised url '%s', " + "ignoring this remote"), url); + return NULL; + } + + if (url_info.host_len) { + strbuf_add(&buf, normalized + url_info.host_off, url_info.host_len); + strbuf_addch(&buf, '-'); + } + + if (url_info.port_len) { + strbuf_add(&buf, normalized + url_info.port_off, url_info.port_len); + strbuf_addch(&buf, '-'); + } + + if (url_info.path_len) { + strbuf_add(&buf, normalized + url_info.path_off, url_info.path_len); + strbuf_trim_trailing_dir_sep(&buf); + strbuf_strip_suffix(&buf, ".git"); + } + + free(normalized); + + if (sanitize_remote_name(&buf, url)) { + strbuf_release(&buf); + return NULL; + } + + return strbuf_detach(&buf, NULL); +} + +static void configure_auto_promisor_remote(struct repository *repo, + const char *name, + const char *url, + const char *advertised_as, + bool reuse) +{ + char *key; + + if (!reuse) { + fprintf(stderr, _("Auto-creating promisor remote '%s' for URL '%s'\n"), + name, url); + + key = xstrfmt("remote.%s.url", name); + repo_config_set_gently(repo, key, url); + free(key); + } + + /* NB: when reusing, this promotes an existing non-promisor remote */ + key = xstrfmt("remote.%s.promisor", name); + repo_config_set_gently(repo, key, "true"); + free(key); + + if (advertised_as) { + key = xstrfmt("remote.%s.advertisedAs", name); + repo_config_set_gently(repo, key, advertised_as); + free(key); + } +} + +#define MAX_REMOTES_WITH_SIMILAR_NAMES 20 + +/* Return the allocated local name, or NULL on failure */ +static char *handle_matching_allowed_url(struct repository *repo, + char *allowed_name, + const char *remote_url, + const char *remote_name) +{ + char *name; + char *basename = allowed_name ? + xstrdup(allowed_name) : + promisor_remote_name_from_url(remote_url); + int i = 0; + bool reuse = false; + + if (!basename) + return NULL; + + name = xstrdup(basename); + + while (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { + char *url_key = xstrfmt("remote.%s.url", name); + const char *existing_url; + int exists = !repo_config_get_string_tmp(repo, url_key, &existing_url); + + free(url_key); + + if (!exists) + break; /* Free to use */ + + if (!strcmp(existing_url, remote_url)) { + reuse = true; + break; /* Same URL, so safe to reuse */ + } + + i++; + free(name); + name = xstrfmt("%s-%d", basename, i); + } + + if (i < MAX_REMOTES_WITH_SIMILAR_NAMES) { + configure_auto_promisor_remote(repo, name, + remote_url, remote_name, + reuse); + } else { + warning(_("too many remotes accepted with name like '%s-X', " + "ignoring this remote"), basename); + FREE_AND_NULL(name); + } + + free(basename); + return name; +} + +static int should_accept_new_remote_url(struct repository *repo, + struct string_list *accept_urls, + struct promisor_info *advertised) +{ + struct allowed_url *allowed = url_matches_accept_list(accept_urls, + advertised->url); + if (allowed) { + char *name = handle_matching_allowed_url(repo, + allowed->remote_name, + advertised->url, + advertised->name); + if (name) { + free((char *)advertised->local_name); + advertised->local_name = name; + return 1; + } + } + + return 0; +} + +static int should_accept_remote(struct repository *repo, + enum accept_promisor accept, struct promisor_info *advertised, struct string_list *accept_urls, - struct string_list *config_info) + struct string_list *config_info, + bool *reload_config) { struct promisor_info *p; struct string_list_item *item; @@ -833,6 +1020,13 @@ static int should_accept_remote(enum accept_promisor accept, if (!item) { /* We don't know about that remote */ + + int res = should_accept_new_remote_url(repo, accept_urls, advertised); + if (res) { + *reload_config = true; + return res; + } + if (accept == ACCEPT_ALL) return all_fields_match(advertised, config_info, NULL); return 0; @@ -1093,7 +1287,8 @@ static void filter_promisor_remote(struct repository *repo, string_list_sort(&config_info); } - if (should_accept_remote(accept, advertised, &accept_urls, &config_info)) { + if (should_accept_remote(repo, accept, advertised, &accept_urls, + &config_info, &reload_config)) { if (!store_info) store_info = store_info_new(repo); if (promisor_store_advertised_fields(advertised, store_info)) diff --git a/t/t5710-promisor-remote-capability.sh b/t/t5710-promisor-remote-capability.sh index 0659b2ac157f3b..549acff23f1b22 100755 --- a/t/t5710-promisor-remote-capability.sh +++ b/t/t5710-promisor-remote-capability.sh @@ -458,6 +458,107 @@ test_expect_success "clone with 'None', URL allowlisted, but client has differen initialize_server 1 "$oid" ' +test_expect_success "clone with URL allowlisted and no remote already configured" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + test_when_finished "rm -f full_names" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that exactly one remote has been auto-created, identified + # by "remote..advertisedAs" == "lop". + git -C client config get --all --show-names --regexp \ + "remote\..*\.advertisedas" >full_names && + test_line_count = 1 full_names && + REMOTE_NAME=$(sed "s/^remote\.\(.*\)\.advertisedas .*$/\1/" full_names) && + + # Check ".url" and ".promisor" values + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" >expect && + git -C client config "remote.$REMOTE_NAME.url" >actual && + git -C client config "remote.$REMOTE_NAME.promisor" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with named URL allowlisted and no pre-configured remote" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that a remote has been auto-created with the right "cdn" name and fields. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + git -C client config "remote.cdn.advertisedAs" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with URL allowlisted but colliding name" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone -c remote.cdn.promisor=true \ + -c remote.cdn.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.cdn.url="https://example.com/cdn" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that a remote has been auto-created with the right "cdn-1" name and fields. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" >expect && + git -C client config "remote.cdn-1.url" >actual && + git -C client config "remote.cdn-1.promisor" >>actual && + git -C client config "remote.cdn-1.advertisedAs" >>actual && + test_cmp expect actual && + + # Check that the original "cdn" remote was not overwritten. + printf "%s\n" "https://example.com/cdn" "true" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + test_cmp expect actual && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + +test_expect_success "clone with URL allowlisted and reusable remote" ' + git -C server config promisor.advertise true && + test_when_finished "rm -rf client" && + + GIT_NO_LAZY_FETCH=0 git clone \ + -c remote.cdn.fetch="+refs/heads/*:refs/remotes/lop/*" \ + -c remote.cdn.url="$TRASH_DIRECTORY_URL/lop" \ + -c promisor.acceptfromserver=None \ + -c promisor.acceptFromServerUrl="cdn=$ENCODED_TRASH_DIRECTORY_URL/*" \ + --no-local --filter="blob:limit=5k" server client && + + # Check that the existing "cdn" remote has been properly updated. + printf "%s\n" "$TRASH_DIRECTORY_URL/lop" "true" "lop" "+refs/heads/*:refs/remotes/lop/*" >expect && + git -C client config "remote.cdn.url" >actual && + git -C client config "remote.cdn.promisor" >>actual && + git -C client config "remote.cdn.advertisedAs" >>actual && + git -C client config "remote.cdn.fetch" >>actual && + test_cmp expect actual && + + # Check that no new "cdn-1" remote has been created. + test_must_fail git -C client config "remote.cdn-1.url" && + + # Check that the largest object is still missing on the server + check_missing_objects server 1 "$oid" +' + test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' git -C server config promisor.advertise true && test_when_finished "rm -rf client" && @@ -472,6 +573,9 @@ test_expect_success "clone with invalid promisor.acceptFromServerUrl" ' # Check that a warning was emitted test_grep "invalid remote name '\''bad name'\''" err && + # Check that no remote was auto-created + test_must_fail git -C client config get --regexp "remote\..*\.advertisedas" && + # Check that the largest object is not missing on the server check_missing_objects server 0 "" && From 8f32e6f343b451f04e57dfda31bef04cb23f65a1 Mon Sep 17 00:00:00 2001 From: Christian Couder Date: Wed, 27 May 2026 16:08:20 +0200 Subject: [PATCH 012/436] doc: promisor: improve acceptFromServer entry The entry for the `promisor.acceptFromServer` in "Documentation/config/promisor.adoc" has a number of issues: - it's not clear if new remotes and URLs can be created, - it looks like a big block of text, - it's not easy to see all the options, - it's not easy to see which option is the default one, - for "knownName", it says "advertised by the client" instead of "advertised by the server", - it doesn't refer to the new related `acceptFromServerUrl` option. Let's address all these issues by rewording large parts of it and using bullet points for the different options. Signed-off-by: Christian Couder Signed-off-by: Junio C Hamano --- Documentation/config/promisor.adoc | 53 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/Documentation/config/promisor.adoc b/Documentation/config/promisor.adoc index 455ce40be892de..f07a2e883bd96c 100644 --- a/Documentation/config/promisor.adoc +++ b/Documentation/config/promisor.adoc @@ -32,24 +32,41 @@ variable is set to "true", and the "name" and "url" fields are always advertised regardless of this setting. promisor.acceptFromServer:: - If set to "all", a client will accept all the promisor remotes - a server might advertise using the "promisor-remote" - capability. If set to "knownName" the client will accept - promisor remotes which are already configured on the client - and have the same name as those advertised by the client. This - is not very secure, but could be used in a corporate setup - where servers and clients are trusted to not switch name and - URLs. If set to "knownUrl", the client will accept promisor - remotes which have both the same name and the same URL - configured on the client as the name and URL advertised by the - server. This is more secure than "all" or "knownName", so it - should be used if possible instead of those options. Default - is "none", which means no promisor remote advertised by a - server will be accepted. By accepting a promisor remote, the - client agrees that the server might omit objects that are - lazily fetchable from this promisor remote from its responses - to "fetch" and "clone" requests from the client. Name and URL - comparisons are case sensitive. See linkgit:gitprotocol-v2[5]. + Controls which promisor remotes advertised by a server (using the + "promisor-remote" protocol capability) a client will accept. By + accepting a promisor remote, the client agrees that the server + might omit objects that are lazily fetchable from this promisor + remote from its responses to "fetch" and "clone" requests. ++ +Note that this option does not cause new remotes to be automatically +created in the client's configuration. It only allows remotes which +are somehow already configured to be trusted for the current +operation, or their fields to be updated (if `promisor.storeFields` is +set and the remote already exists locally). To allow Git to +automatically create and persist new remotes from server +advertisements, use `promisor.acceptFromServerUrl`. ++ +The available options are: ++ +* `none` (default): No promisor remote advertised by a server will be + accepted. ++ +* `knownUrl`: The client will accept promisor remotes that are already + configured on the client and have both the same name and the same URL + as advertised by the server. This is more secure than `all` or + `knownName`, and should be used if possible instead of those options. ++ +* `knownName`: The client will accept promisor remotes that are already + configured on the client and have the same name as those advertised + by the server. This is not very secure, but could be used in a corporate + setup where servers and clients are trusted to not switch names and URLs. ++ +* `all`: The client will accept all the promisor remotes a server might + advertise. This is the least secure option and should only be used in + fully trusted environments. ++ +Name and URL comparisons are case-sensitive. See linkgit:gitprotocol-v2[5] +for protocol details. promisor.acceptFromServerUrl:: A glob pattern to specify which server-advertised URLs a From 9f4e170dfc3bd8cdd284f1c4411b25ce1d09737f Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:00 +0000 Subject: [PATCH 013/436] pack-objects: call release_revisions() after cruft traversal enumerate_and_traverse_cruft_objects() initializes a rev_info on the stack but never calls release_revisions() afterwards. This is not visible on master but becomes a leak once the revision walking machinery uses dynamically allocated structures. Add the missing release_revisions() call. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 480cc0bd8c8d22..67025e86256cfd 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4275,6 +4275,7 @@ static void enumerate_and_traverse_cruft_objects(struct string_list *fresh_packs traverse_commit_list(&revs, show_cruft_commit, show_cruft_object, NULL); stop_progress(&progress_state); + release_revisions(&revs); } static void read_cruft_objects(void) From d877b1af507a6aaf55e8643eb73277a30d3a800b Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:01 +0000 Subject: [PATCH 014/436] revision: introduce rev_walk_mode to clarify get_revision_1() get_revision_1() dispatches to different walk strategies based on a combination of rev_info flags: reflog_info, topo_walk_info, and limited. These conditions are checked in multiple places within the function -- once to select the next commit, and again to decide how to expand parents -- and the two chains must stay in sync. Extract the mode selection into a rev_walk_mode enum and a small get_walk_mode() helper, resolved once at the top of get_revision_1(). Both dispatch sites now switch on the same mode variable, making it obvious that they agree and easier to verify that all modes are handled. No functional change. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- revision.c | 62 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/revision.c b/revision.c index e1970b9c5d34ed..9d0fc696d09937 100644 --- a/revision.c +++ b/revision.c @@ -4327,22 +4327,48 @@ static void track_linear(struct rev_info *revs, struct commit *commit) revs->previous_parents = commit_list_copy(commit->parents); } +enum rev_walk_mode { + REV_WALK_REFLOG, + REV_WALK_TOPO, + REV_WALK_LIMITED, + REV_WALK_STREAMING, +}; + +static enum rev_walk_mode get_walk_mode(struct rev_info *revs) +{ + if (revs->reflog_info) + return REV_WALK_REFLOG; + if (revs->topo_walk_info) + return REV_WALK_TOPO; + if (revs->limited) + return REV_WALK_LIMITED; + return REV_WALK_STREAMING; +} + static struct commit *get_revision_1(struct rev_info *revs) { + enum rev_walk_mode mode = get_walk_mode(revs); + while (1) { struct commit *commit; - if (revs->reflog_info) + switch (mode) { + case REV_WALK_REFLOG: commit = next_reflog_entry(revs->reflog_info); - else if (revs->topo_walk_info) + break; + case REV_WALK_TOPO: commit = next_topo_commit(revs); - else + break; + case REV_WALK_LIMITED: + case REV_WALK_STREAMING: commit = pop_commit(&revs->commits); + break; + } if (!commit) return NULL; - if (revs->reflog_info) + if (mode == REV_WALK_REFLOG) commit->object.flags &= ~(ADDED | SEEN | SHOWN); /* @@ -4350,20 +4376,28 @@ static struct commit *get_revision_1(struct rev_info *revs) * the parents here. We also need to do the date-based limiting * that we'd otherwise have done in limit_list(). */ - if (!revs->limited) { - if (revs->max_age != -1 && - comparison_date(revs, commit) < revs->max_age) - continue; + if (mode != REV_WALK_LIMITED && + revs->max_age != -1 && + comparison_date(revs, commit) < revs->max_age) + continue; - if (revs->reflog_info) - try_to_simplify_commit(revs, commit); - else if (revs->topo_walk_info) - expand_topo_walk(revs, commit); - else if (process_parents(revs, commit, &revs->commits, NULL) < 0) { + switch (mode) { + case REV_WALK_REFLOG: + try_to_simplify_commit(revs, commit); + break; + case REV_WALK_TOPO: + expand_topo_walk(revs, commit); + break; + case REV_WALK_STREAMING: + if (process_parents(revs, commit, + &revs->commits, NULL) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", - oid_to_hex(&commit->object.oid)); + oid_to_hex(&commit->object.oid)); } + break; + case REV_WALK_LIMITED: + break; } switch (simplify_commit(revs, commit)) { From dd4bc01c0a8fc871a68a5027ed5ac953fa47fc6e Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Wed, 27 May 2026 15:50:02 +0000 Subject: [PATCH 015/436] revision: use priority queue for non-limited streaming walks The streaming (non-limited) walk in get_revision_1() inserts newly discovered parent commits into a date-sorted queue via commit_list_insert_by_date(), which scans the linked list to find the insertion point -- O(w) per insert, where w is the width of the active walk frontier. Replace this with an O(log w) priority queue. Add a commit_queue field to rev_info alongside the existing commits linked list. The two representations are mutually exclusive: setup and external callers that need list access use the linked list, then get_revision_1() lazily drains it into the priority queue on first call. Add a REV_WALK_NO_WALK enum value to distinguish the no_walk case (which still uses the commit list) from the streaming case. The conversion function rev_info_commit_list_to_queue() is public so callers that know they will iterate can convert early. Combined with the limit_list() priority queue change already in master, this eliminates all O(w) sorted linked-list insertion from the revision walk machinery. Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- commit.c | 13 ------------- commit.h | 2 -- revision.c | 55 +++++++++++++++++++++++++++++------------------------- revision.h | 12 +++++++++++- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/commit.c b/commit.c index e3e7352e69682d..5112c7b2af31b1 100644 --- a/commit.c +++ b/commit.c @@ -729,19 +729,6 @@ void commit_list_free(struct commit_list *list) pop_commit(&list); } -struct commit_list * commit_list_insert_by_date(struct commit *item, struct commit_list **list) -{ - struct commit_list **pp = list; - struct commit_list *p; - while ((p = *pp) != NULL) { - if (p->item->date < item->date) { - break; - } - pp = &p->next; - } - return commit_list_insert(item, pp); -} - static int commit_list_compare_by_date(const struct commit_list *a, const struct commit_list *b) { diff --git a/commit.h b/commit.h index 58150045afafed..385492fbb1ecc5 100644 --- a/commit.h +++ b/commit.h @@ -191,8 +191,6 @@ int commit_list_contains(struct commit *item, struct commit_list **commit_list_append(struct commit *commit, struct commit_list **next); unsigned commit_list_count(const struct commit_list *l); -struct commit_list *commit_list_insert_by_date(struct commit *item, - struct commit_list **list); void commit_list_sort_by_date(struct commit_list **list); /* Shallow copy of the input list */ diff --git a/revision.c b/revision.c index 9d0fc696d09937..4bb3b16e43acb9 100644 --- a/revision.c +++ b/revision.c @@ -1116,7 +1116,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit) } static int process_parents(struct rev_info *revs, struct commit *commit, - struct commit_list **list, struct prio_queue *queue) + struct prio_queue *queue) { struct commit_list *parent = commit->parents; unsigned pass_flags; @@ -1158,8 +1158,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit, if (p->object.flags & SEEN) continue; p->object.flags |= (SEEN | NOT_USER_GIVEN); - if (list) - commit_list_insert_by_date(p, list); if (queue) prio_queue_put(queue, p); if (revs->exclude_first_parent_only) @@ -1207,8 +1205,6 @@ static int process_parents(struct rev_info *revs, struct commit *commit, p->object.flags |= pass_flags | CHILD_VISITED; if (!(p->object.flags & SEEN)) { p->object.flags |= (SEEN | NOT_USER_GIVEN); - if (list) - commit_list_insert_by_date(p, list); if (queue) prio_queue_put(queue, p); } @@ -1470,7 +1466,7 @@ static int limit_list(struct rev_info *revs) if (revs->max_age != -1 && (commit->date < revs->max_age)) obj->flags |= UNINTERESTING; - if (process_parents(revs, commit, NULL, &queue) < 0) { + if (process_parents(revs, commit, &queue) < 0) { clear_prio_queue(&queue); return -1; } @@ -3257,6 +3253,7 @@ static void free_void_commit_list(void *list) void release_revisions(struct rev_info *revs) { commit_list_free(revs->commits); + clear_prio_queue(&revs->commit_queue); commit_list_free(revs->ancestry_path_bottoms); release_display_notes(&revs->notes_opt); object_array_clear(&revs->pending); @@ -3726,7 +3723,7 @@ static void explore_walk_step(struct rev_info *revs) if (revs->max_age != -1 && (c->date < revs->max_age)) c->object.flags |= UNINTERESTING; - if (process_parents(revs, c, NULL, NULL) < 0) + if (process_parents(revs, c, NULL) < 0) return; if (c->object.flags & UNINTERESTING) @@ -3902,7 +3899,7 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit) { struct commit_list *p; struct topo_walk_info *info = revs->topo_walk_info; - if (process_parents(revs, commit, NULL, NULL) < 0) { + if (process_parents(revs, commit, NULL) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", oid_to_hex(&commit->object.oid)); @@ -3938,6 +3935,13 @@ static void expand_topo_walk(struct rev_info *revs, struct commit *commit) } } +void rev_info_commit_list_to_queue(struct rev_info *revs) +{ + while (revs->commits) + prio_queue_put(&revs->commit_queue, pop_commit(&revs->commits)); +} + + int prepare_revision_walk(struct rev_info *revs) { int i; @@ -4006,7 +4010,7 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs, for (;;) { struct commit *p = *pp; if (!revs->limited) - if (process_parents(revs, p, NULL, queue) < 0) + if (process_parents(revs, p, queue) < 0) return rewrite_one_error; if (p->object.flags & UNINTERESTING) return rewrite_one_ok; @@ -4020,27 +4024,18 @@ static enum rewrite_result rewrite_one_1(struct rev_info *revs, } } -static void merge_queue_into_list(struct prio_queue *q, struct commit_list **list) +static void merge_queue_into_prio_queue(struct prio_queue *from, + struct prio_queue *to) { - while (q->nr) { - struct commit *item = prio_queue_peek(q); - struct commit_list *p = *list; - - if (p && p->item->date >= item->date) - list = &p->next; - else { - p = commit_list_insert(item, list); - list = &p->next; /* skip newly added item */ - prio_queue_get(q); /* pop item */ - } - } + while (from->nr) + prio_queue_put(to, prio_queue_get(from)); } static enum rewrite_result rewrite_one(struct rev_info *revs, struct commit **pp) { struct prio_queue queue = { compare_commits_by_commit_date }; enum rewrite_result ret = rewrite_one_1(revs, pp, &queue); - merge_queue_into_list(&queue, &revs->commits); + merge_queue_into_prio_queue(&queue, &revs->commit_queue); clear_prio_queue(&queue); return ret; } @@ -4331,6 +4326,7 @@ enum rev_walk_mode { REV_WALK_REFLOG, REV_WALK_TOPO, REV_WALK_LIMITED, + REV_WALK_NO_WALK, REV_WALK_STREAMING, }; @@ -4342,6 +4338,8 @@ static enum rev_walk_mode get_walk_mode(struct rev_info *revs) return REV_WALK_TOPO; if (revs->limited) return REV_WALK_LIMITED; + if (revs->no_walk) + return REV_WALK_NO_WALK; return REV_WALK_STREAMING; } @@ -4349,6 +4347,9 @@ static struct commit *get_revision_1(struct rev_info *revs) { enum rev_walk_mode mode = get_walk_mode(revs); + if (mode == REV_WALK_STREAMING && revs->commits) + rev_info_commit_list_to_queue(revs); + while (1) { struct commit *commit; @@ -4360,9 +4361,12 @@ static struct commit *get_revision_1(struct rev_info *revs) commit = next_topo_commit(revs); break; case REV_WALK_LIMITED: - case REV_WALK_STREAMING: + case REV_WALK_NO_WALK: commit = pop_commit(&revs->commits); break; + case REV_WALK_STREAMING: + commit = prio_queue_get(&revs->commit_queue); + break; } if (!commit) @@ -4390,12 +4394,13 @@ static struct commit *get_revision_1(struct rev_info *revs) break; case REV_WALK_STREAMING: if (process_parents(revs, commit, - &revs->commits, NULL) < 0) { + &revs->commit_queue) < 0) { if (!revs->ignore_missing_links) die("Failed to traverse parents of commit %s", oid_to_hex(&commit->object.oid)); } break; + case REV_WALK_NO_WALK: case REV_WALK_LIMITED: break; } diff --git a/revision.h b/revision.h index 584f1338b5e323..04982a3d47f28f 100644 --- a/revision.h +++ b/revision.h @@ -12,6 +12,7 @@ #include "decorate.h" #include "ident.h" #include "list-objects-filter-options.h" +#include "prio-queue.h" #include "strvec.h" /** @@ -122,8 +123,14 @@ struct oidset; struct topo_walk_info; struct rev_info { - /* Starting list */ + /* + * Work queue of commits, stored as either a linked list or a + * priority queue, but never both at the same time. + * rev_info_commit_list_to_queue() converts list to queue. + */ struct commit_list *commits; + struct prio_queue commit_queue; + struct object_array pending; struct repository *repo; @@ -400,6 +407,7 @@ struct rev_info { * uninitialized. */ #define REV_INFO_INIT { \ + .commit_queue = { .compare = compare_commits_by_commit_date }, \ .abbrev = DEFAULT_ABBREV, \ .simplify_history = 1, \ .pruning.flags.recursive = 1, \ @@ -478,6 +486,8 @@ void reset_revision_walk(void); */ int prepare_revision_walk(struct rev_info *revs); +/* Drain the commits linked list into the priority queue. */ +void rev_info_commit_list_to_queue(struct rev_info *revs); /** * Takes a pointer to a `rev_info` structure and iterates over it, returning a * `struct commit *` each time you call it. The end of the revision list is From 8c84e6802c0e23503bfe655dadcdc4a15de7373a Mon Sep 17 00:00:00 2001 From: Kristofer Karlsson Date: Thu, 28 May 2026 09:00:48 +0000 Subject: [PATCH 016/436] t3070: skip ls-files tests with backslash patterns on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows (MINGW), backslashes in pathspecs are silently converted to forward slashes (directory separators), which changes the glob semantics. This causes 36 test failures in t3070-wildmatch when the "via ls-files" variants test patterns containing backslash escapes (e.g. '\[ab]', '[\-_]', '[A-\\]'). The wildmatch function itself handles these patterns correctly — only the ls-files code path fails because pathspec parsing converts the backslashes before they reach the glob matcher. Skip these ls-files tests on platforms where BSLASHPSPEC is not set, which is the existing prereq that captures exactly this semantic: "backslashes in pathspec are not directory separators." Signed-off-by: Kristofer Karlsson Signed-off-by: Junio C Hamano --- t/t3070-wildmatch.sh | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/t/t3070-wildmatch.sh b/t/t3070-wildmatch.sh index 655bb1a0f21031..33941222189663 100755 --- a/t/t3070-wildmatch.sh +++ b/t/t3070-wildmatch.sh @@ -99,6 +99,13 @@ match_with_ls_files() { match_function=$4 ls_files_args=$5 + prereqs=EXPENSIVE_ON_WINDOWS + case "$pattern" in + *\\*) + prereqs="$prereqs,BSLASHPSPEC" + ;; + esac + match_stdout_stderr_cmp=" tr -d '\0' actual && test_must_be_empty actual.err && @@ -108,36 +115,36 @@ match_with_ls_files() { then if test -e .git/created_test_file then - test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match dies on '$pattern' '$text'" " + test_expect_success $prereqs "$match_function (via ls-files): match dies on '$pattern' '$text'" " printf '%s' '$text' >expect && test_must_fail git$ls_files_args ls-files -z -- '$pattern' " else - test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match skip '$pattern' '$text'" 'false' + test_expect_failure $prereqs "$match_function (via ls-files): match skip '$pattern' '$text'" 'false' fi elif test "$match_expect" = 1 then if test -e .git/created_test_file then - test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match '$pattern' '$text'" " + test_expect_success $prereqs "$match_function (via ls-files): match '$pattern' '$text'" " printf '%s' '$text' >expect && git$ls_files_args ls-files -z -- '$pattern' >actual.raw 2>actual.err && $match_stdout_stderr_cmp " else - test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): match skip '$pattern' '$text'" 'false' + test_expect_failure $prereqs "$match_function (via ls-files): match skip '$pattern' '$text'" 'false' fi elif test "$match_expect" = 0 then if test -e .git/created_test_file then - test_expect_success EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): no match '$pattern' '$text'" " + test_expect_success $prereqs "$match_function (via ls-files): no match '$pattern' '$text'" " >expect && git$ls_files_args ls-files -z -- '$pattern' >actual.raw 2>actual.err && $match_stdout_stderr_cmp " else - test_expect_failure EXPENSIVE_ON_WINDOWS "$match_function (via ls-files): no match skip '$pattern' '$text'" 'false' + test_expect_failure $prereqs "$match_function (via ls-files): no match skip '$pattern' '$text'" 'false' fi else test_expect_success "PANIC: Test framework error. Unknown matches value $match_expect" 'false' From 1ec041bebb46159562c4beeb2e6980284e0f9a28 Mon Sep 17 00:00:00 2001 From: Michael Montalbo Date: Thu, 28 May 2026 19:21:45 +0000 Subject: [PATCH 017/436] doc: clarify that --word-diff operates on line-level hunks The --word-diff documentation describes the output modes and word-regex mechanics but does not explain that word-diff operates within the hunks produced by the line-level diff rather than performing an independent word-stream comparison. This can surprise users when the line-level alignment causes word-level changes to appear even though the words in both files are identical. Add an implementation note explaining the two-stage relationship and that the output may change if Git acquires a different implementation in the future. Signed-off-by: Michael Montalbo Signed-off-by: Junio C Hamano --- Documentation/diff-options.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/diff-options.adoc b/Documentation/diff-options.adoc index 9cdad6f72a0c7d..88b724b8c6dba4 100644 --- a/Documentation/diff-options.adoc +++ b/Documentation/diff-options.adoc @@ -455,6 +455,14 @@ endif::git-diff[] + Note that despite the name of the first mode, color is used to highlight the changed parts in all modes if enabled. ++ +The `--word-diff` option operates by taking the same line-by-line +diff that is produced without the option and computing +word-by-word changes within each hunk. This may produce a +larger diff than a dedicated word-diff tool would. If Git +acquires a different implementation in the future, the output +may change. Note that this is similar to the `--diff-algorithm` +option, which may also change the output. `--word-diff-regex=`:: Use __ to decide what a word is, instead of considering From dc6068df67a5c4a36d4579ce783377a667411230 Mon Sep 17 00:00:00 2001 From: Weijie Yuan Date: Fri, 29 May 2026 16:17:04 +0800 Subject: [PATCH 018/436] docs: fix typos and grammar Fix several spelling mistakes, subject-verb agreement issues, and duplicated words. Signed-off-by: Weijie Yuan Signed-off-by: Junio C Hamano --- Documentation/fetch-options.adoc | 2 +- combine-diff.c | 2 +- contrib/subtree/t/t7900-subtree.sh | 2 +- csum-file.h | 2 +- delta-islands.c | 2 +- diffcore-pickaxe.c | 2 +- odb.h | 2 +- parse-options.c | 2 +- rerere.c | 2 +- t/t4203-mailmap.sh | 2 +- t/t9100-git-svn-basic.sh | 2 +- t/test-lib-github-workflow-markup.sh | 2 +- t/test-lib-junit.sh | 2 +- tree-walk.h | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 81a9d7f9bbc11d..b3e387413fb6de 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -1,6 +1,6 @@ `--all`:: `--no-all`:: - Fetch all remotes, except for the ones that has the + Fetch all remotes, except for the ones that have the `remote..skipFetchAll` configuration variable set. This overrides the configuration variable `fetch.all`. diff --git a/combine-diff.c b/combine-diff.c index b7998620687ed7..720768ce41b5df 100644 --- a/combine-diff.c +++ b/combine-diff.c @@ -666,7 +666,7 @@ static int make_hunks(struct sline *sline, unsigned long cnt, * (-) line, which records from what parents the line * was removed; this line does not appear in the result. * then check the set of parents the result has difference - * from, from all lines. If there are lines that has + * from, from all lines. If there are lines that have * different set of parents that the result has differences * from, that means we have more than two versions. * diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index 18d2b564487e91..4194687cfbb9b5 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -75,7 +75,7 @@ test_create_pre2_32_repo () { # # Create a simple subtree on a new branch named ORPHAN in REPO. # The subtree is then merged into the current branch of REPO, -# under PREFIX. The generated subtree has has one commit +# under PREFIX. The generated subtree has one commit # with subject and tag FILENAME with a single file "FILENAME.t" # # When this method returns: diff --git a/csum-file.h b/csum-file.h index a9b390d3366875..a270738a7a3cad 100644 --- a/csum-file.h +++ b/csum-file.h @@ -52,7 +52,7 @@ struct hashfd_options { */ struct progress *progress; - /* The length of the buffer that shall be used read read data. */ + /* The length of the buffer that shall be used to read data. */ size_t buffer_len; }; diff --git a/delta-islands.c b/delta-islands.c index f4d2468790ce4f..e71a7e1c055dc8 100644 --- a/delta-islands.c +++ b/delta-islands.c @@ -527,7 +527,7 @@ void free_island_marks(void) kh_destroy_oid_map(island_marks); } - /* detect use-after-free with a an address which is never valid: */ + /* detect use-after-free with an address which is never valid: */ island_marks = (void *)-1; } diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c index a52d569911c48e..b0915be86fc475 100644 --- a/diffcore-pickaxe.c +++ b/diffcore-pickaxe.c @@ -203,7 +203,7 @@ static void pickaxe(struct diff_queue_struct *q, struct diff_options *o, for (i = 0; i < q->nr; i++) diff_free_filepair(q->queue[i]); } else { - /* Showing only the filepairs that has the needle */ + /* Showing only the filepairs that have the needle */ for (i = 0; i < q->nr; i++) { struct diff_filepair *p = q->queue[i]; if (pickaxe_match(p, o, regexp, kws, fn)) diff --git a/odb.h b/odb.h index 3a711f6547bb00..24b84511931376 100644 --- a/odb.h +++ b/odb.h @@ -57,7 +57,7 @@ struct object_database { struct repository *repo; /* - * State of current current object database transaction. Only one + * State of current object database transaction. Only one * transaction may be pending at a time. Is NULL when no transaction is * configured. */ diff --git a/parse-options.c b/parse-options.c index a676da86f5d617..f4647e0099ea99 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1149,7 +1149,7 @@ enum parse_opt_result parse_options_step(struct parse_opt_ctx_t *ctx, (ctx->flags & PARSE_OPT_KEEP_UNKNOWN_OPT)) { /* * Found an unknown option given to a command with - * subcommands that has a default operation mode: + * subcommands that have a default operation mode: * we treat this option and all remaining args as * arguments meant to that default operation mode. * So we are done parsing. diff --git a/rerere.c b/rerere.c index 0296700f9f448f..28a740b771cf99 100644 --- a/rerere.c +++ b/rerere.c @@ -548,7 +548,7 @@ static int check_one_conflict(struct index_state *istate, int i, int *type) /* * Scan the index and find paths that have conflicts that rerere can - * handle, i.e. the ones that has both stages #2 and #3. + * handle, i.e. the ones that have both stages #2 and #3. * * NEEDSWORK: we do not record or replay a previous "resolve by * deletion" for a delete-modify conflict, as that is inherently risky diff --git a/t/t4203-mailmap.sh b/t/t4203-mailmap.sh index 74b7ddccb26d59..03f6df9d244890 100755 --- a/t/t4203-mailmap.sh +++ b/t/t4203-mailmap.sh @@ -180,7 +180,7 @@ test_expect_success 'mailmap.file set' ' git shortlog HEAD >actual && test_cmp expect actual && - # The internal_mailmap/.mailmap file is an a subdirectory, but + # The internal_mailmap/.mailmap file is in a subdirectory, but # as shown here it can also be outside the repository test_when_finished "rm -rf sub-repo" && git clone . sub-repo && diff --git a/t/t9100-git-svn-basic.sh b/t/t9100-git-svn-basic.sh index af28b01fefa49c..1ab98b9c373a7e 100755 --- a/t/t9100-git-svn-basic.sh +++ b/t/t9100-git-svn-basic.sh @@ -232,7 +232,7 @@ test_expect_success POSIXPERM,SYMLINKS "$name" ' test_cmp expected.$(test_oid algo) a ' -test_expect_success 'exit if remote refs are ambigious' ' +test_expect_success 'exit if remote refs are ambiguous' ' git config --add svn-remote.svn.fetch \ bar:refs/remotes/git-svn && test_must_fail git svn migrate diff --git a/t/test-lib-github-workflow-markup.sh b/t/test-lib-github-workflow-markup.sh index 33405c90d740d4..fa29a62aa311c5 100644 --- a/t/test-lib-github-workflow-markup.sh +++ b/t/test-lib-github-workflow-markup.sh @@ -18,7 +18,7 @@ # # The idea is for `test-lib.sh` to source this file when run in GitHub # workflows; these functions will then override (empty) functions -# that are are called at the appropriate times during the test runs. +# that are called at the appropriate times during the test runs. test_skip_test_preamble=t diff --git a/t/test-lib-junit.sh b/t/test-lib-junit.sh index 76cbbd3299d64a..f4994dd9d3183d 100644 --- a/t/test-lib-junit.sh +++ b/t/test-lib-junit.sh @@ -19,7 +19,7 @@ # # The idea is for `test-lib.sh` to source this file when the user asks # for JUnit XML; these functions will then override (empty) functions -# that are are called at the appropriate times during the test runs. +# that are called at the appropriate times during the test runs. start_test_output () { junit_xml_dir="$TEST_OUTPUT_DIRECTORY/out" diff --git a/tree-walk.h b/tree-walk.h index 29a55328bd94a4..9646c47ac5bce5 100644 --- a/tree-walk.h +++ b/tree-walk.h @@ -177,7 +177,7 @@ struct traverse_info { /** * Walk trees starting with "tree_oid" to find the entry for "name", and - * return the the object name and the mode of the found entry via the + * return the object name and the mode of the found entry via the * "oid" and "mode" parameters. Return 0 if the entry is found, and -1 * otherwise. */ From 89858b2f3c9f4ddbbb22962dff9e739f91d9c388 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:14 -0400 Subject: [PATCH 019/436] git-gui: use HEAD as current branch when detached commit f87a36b697 ("git-gui: use git-branch --show-current", 2024-02-12) changed git-gui to use git-branch to access refs, rather than directly reading files as doing the latter is not compatible with the reftable backend. git branch --show-current reports an empty branch name when the head is detached, and in this case load_current_branch needs to report HEAD using special case logic as it did prior to the above commit. Make it do so. This addresses an issue with git-gui browser failing with a detached head. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/git-gui.sh b/git-gui.sh index 23fe76e498bd17..f70a54a61b2664 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -670,6 +670,9 @@ proc load_current_branch {} { set current_branch [git branch --show-current] set is_detached [expr [string length $current_branch] == 0] + if {$is_detached} { + set current_branch {HEAD} + } } auto_load tk_optionMenu From a7e5d892034b6e81dbbbaed09fdbade12352157b Mon Sep 17 00:00:00 2001 From: Johannes Sixt Date: Sun, 31 May 2026 19:02:15 -0400 Subject: [PATCH 020/436] git-gui: remove unnecessary 'cd $_gitworktree' from do_gitk In the procedure that invokes Gitk, we have a 'cd $_gitworktree'. Such a change of the current directory is not necessary, because - if we have a working tree, then the startup routine has already changed the current directory to the root of the working tree, which *is* $_gitworktree; or - if we are in a bare repository, then there is no point in changing the current directory anywhere. (And $_gitworktree is empty.) Signed-off-by: Johannes Sixt Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index f70a54a61b2664..52897fbd099818 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -2024,11 +2024,7 @@ proc do_gitk {revs {is_submodule false}} { set pwd [pwd] - if {!$is_submodule} { - if {![is_bare]} { - cd $_gitworktree - } - } else { + if {$is_submodule} { cd $current_diff_path if {$revs eq {--}} { set s $file_states($current_diff_path) From 514f039c9052c23047c310f911ba8c0c2e74a1c7 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:24 +0200 Subject: [PATCH 021/436] odb/source-loose: move loose source into "odb/" subsystem In subsequent patches we'll be turning `struct odb_source_loose` into a proper `struct odb_source`. As a first step towards this goal, move its struct out of "object-file.c" and into "odb/source-loose.c". This detaches the implementation of the loose object source from the generic object file code, following the same convention already used by the "files" and "in-memory" sources. No functional changes are intended. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Makefile | 1 + meson.build | 1 + object-file.c | 8 -------- object-file.h | 21 +-------------------- odb/source-loose.c | 10 ++++++++++ odb/source-loose.h | 34 ++++++++++++++++++++++++++++++++++ 6 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 odb/source-loose.c create mode 100644 odb/source-loose.h diff --git a/Makefile b/Makefile index a43b8ee0674df8..01356235c3e11d 100644 --- a/Makefile +++ b/Makefile @@ -1217,6 +1217,7 @@ LIB_OBJS += odb.o LIB_OBJS += odb/source.o LIB_OBJS += odb/source-files.o LIB_OBJS += odb/source-inmemory.o +LIB_OBJS += odb/source-loose.o LIB_OBJS += odb/streaming.o LIB_OBJS += odb/transaction.o LIB_OBJS += oid-array.o diff --git a/meson.build b/meson.build index 664d8313295a26..c85e5988351b1f 100644 --- a/meson.build +++ b/meson.build @@ -405,6 +405,7 @@ libgit_sources = [ 'odb/source.c', 'odb/source-files.c', 'odb/source-inmemory.c', + 'odb/source-loose.c', 'odb/streaming.c', 'odb/transaction.c', 'oid-array.c', diff --git a/object-file.c b/object-file.c index 90f995d0000bf6..641bd9c0799dec 100644 --- a/object-file.c +++ b/object-file.c @@ -2205,14 +2205,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) return &transaction->base; } -struct odb_source_loose *odb_source_loose_new(struct odb_source *source) -{ - struct odb_source_loose *loose; - CALLOC_ARRAY(loose, 1); - loose->source = source; - return loose; -} - void odb_source_loose_free(struct odb_source_loose *loose) { if (!loose) diff --git a/object-file.h b/object-file.h index 5241b8dd5c564d..1d8312cf7f9ff9 100644 --- a/object-file.h +++ b/object-file.h @@ -4,6 +4,7 @@ #include "git-zlib.h" #include "object.h" #include "odb.h" +#include "odb/source-loose.h" struct index_state; @@ -20,26 +21,6 @@ struct object_info; struct odb_read_stream; struct odb_source; -struct odb_source_loose { - struct odb_source *source; - - /* - * Used to store the results of readdir(3) calls when we are OK - * sacrificing accuracy due to races for speed. That includes - * object existence with OBJECT_INFO_QUICK, as well as - * our search for unique abbreviated hashes. Don't use it for tasks - * requiring greater accuracy! - * - * Be sure to call odb_load_loose_cache() before using. - */ - uint32_t subdir_seen[8]; /* 256 bits */ - struct oidtree *cache; - - /* Map between object IDs for loose objects. */ - struct loose_object_map *map; -}; - -struct odb_source_loose *odb_source_loose_new(struct odb_source *source); void odb_source_loose_free(struct odb_source_loose *loose); /* Reprepare the loose source by emptying the loose object cache. */ diff --git a/odb/source-loose.c b/odb/source-loose.c new file mode 100644 index 00000000000000..b944d2181324ce --- /dev/null +++ b/odb/source-loose.c @@ -0,0 +1,10 @@ +#include "git-compat-util.h" +#include "odb/source-loose.h" + +struct odb_source_loose *odb_source_loose_new(struct odb_source *source) +{ + struct odb_source_loose *loose; + CALLOC_ARRAY(loose, 1); + loose->source = source; + return loose; +} diff --git a/odb/source-loose.h b/odb/source-loose.h new file mode 100644 index 00000000000000..8b4bac77ea39e8 --- /dev/null +++ b/odb/source-loose.h @@ -0,0 +1,34 @@ +#ifndef ODB_SOURCE_LOOSE_H +#define ODB_SOURCE_LOOSE_H + +#include "odb/source.h" + +struct object_database; +struct oidtree; + +/* + * An object database source that stores its objects in loose format, one + * file per object. This source is part of the files source. + */ +struct odb_source_loose { + struct odb_source *source; + + /* + * Used to store the results of readdir(3) calls when we are OK + * sacrificing accuracy due to races for speed. That includes + * object existence with OBJECT_INFO_QUICK, as well as + * our search for unique abbreviated hashes. Don't use it for tasks + * requiring greater accuracy! + * + * Be sure to call odb_load_loose_cache() before using. + */ + uint32_t subdir_seen[8]; /* 256 bits */ + struct oidtree *cache; + + /* Map between object IDs for loose objects. */ + struct loose_object_map *map; +}; + +struct odb_source_loose *odb_source_loose_new(struct odb_source *source); + +#endif From 1d451ba6fec076d357abf62607b97f585283030a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:25 +0200 Subject: [PATCH 022/436] odb/source-loose: store pointer to "files" instead of generic source The `struct odb_source_loose` holds a pointer to its owning parent source. The way that Git is currently structured, this parent is always the "files" source. In subsequent commits we're going to detangle that so that the "loose" source doesn't have any owning parent source at all so that it can be used as a completely standalone source. Detangling this mess is somewhat intricate though, and is made even more intricate because it's not always clear which kind of source one is holding at a specific point in time -- either the parent "files" source, or the child "loose" source. Make this relationship more explicit by storing a pointer to the "files" source instead of storing a pointer to a generic `struct odb_source`. This will help make subsequent steps a bit clearer. Note that this is a temporary step, only. At the end of this series we will have dropped the parent pointer completely. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 4 ++-- odb/source-files.c | 2 +- odb/source-loose.c | 4 ++-- odb/source-loose.h | 5 +++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/object-file.c b/object-file.c index 641bd9c0799dec..7a1908bfc05cb5 100644 --- a/object-file.c +++ b/object-file.c @@ -178,7 +178,7 @@ static int open_loose_object(struct odb_source_loose *loose, static struct strbuf buf = STRBUF_INIT; int fd; - *path = odb_loose_path(loose->source, &buf, oid); + *path = odb_loose_path(&loose->files->base, &buf, oid); fd = git_open(*path); if (fd >= 0) return fd; @@ -189,7 +189,7 @@ static int open_loose_object(struct odb_source_loose *loose, static int quick_has_loose(struct odb_source_loose *loose, const struct object_id *oid) { - return !!oidtree_contains(odb_source_loose_cache(loose->source, oid), oid); + return !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid); } /* diff --git a/odb/source-files.c b/odb/source-files.c index b5abd20e971e78..185cc6903e35f2 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -264,7 +264,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, CALLOC_ARRAY(files, 1); odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local); - files->loose = odb_source_loose_new(&files->base); + files->loose = odb_source_loose_new(files); files->packed = packfile_store_new(&files->base); files->base.free = odb_source_files_free; diff --git a/odb/source-loose.c b/odb/source-loose.c index b944d2181324ce..c9e7414814814d 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -1,10 +1,10 @@ #include "git-compat-util.h" #include "odb/source-loose.h" -struct odb_source_loose *odb_source_loose_new(struct odb_source *source) +struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) { struct odb_source_loose *loose; CALLOC_ARRAY(loose, 1); - loose->source = source; + loose->files = files; return loose; } diff --git a/odb/source-loose.h b/odb/source-loose.h index 8b4bac77ea39e8..bf61e767c8aab4 100644 --- a/odb/source-loose.h +++ b/odb/source-loose.h @@ -3,6 +3,7 @@ #include "odb/source.h" +struct odb_source_files; struct object_database; struct oidtree; @@ -11,7 +12,7 @@ struct oidtree; * file per object. This source is part of the files source. */ struct odb_source_loose { - struct odb_source *source; + struct odb_source_files *files; /* * Used to store the results of readdir(3) calls when we are OK @@ -29,6 +30,6 @@ struct odb_source_loose { struct loose_object_map *map; }; -struct odb_source_loose *odb_source_loose_new(struct odb_source *source); +struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files); #endif From ead691927b05dbbd2655db9a7183d5fcb935bf3b Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:26 +0200 Subject: [PATCH 023/436] odb/source-loose: start converting to a proper `struct odb_source` Start converting `struct odb_source_loose` into a proper pluggable `struct odb_source` by embedding the base struct and assigning it the new `ODB_SOURCE_LOOSE` type. Furthermore, wire up lifecycle management of this source by implementing the `free` callback and taking ownership of the chdir notifications. Note that the loose source is not yet functional as a standalone `struct odb_source`, as it's missing all of the callback implementations. These will be wired up in subsequent commits. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 17 ----------------- object-file.h | 2 -- odb/source-files.c | 2 +- odb/source-loose.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ odb/source-loose.h | 14 ++++++++++++++ odb/source.h | 3 +++ 6 files changed, 63 insertions(+), 20 deletions(-) diff --git a/object-file.c b/object-file.c index 7a1908bfc05cb5..977d959d333166 100644 --- a/object-file.c +++ b/object-file.c @@ -2041,14 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source, return files->loose->cache; } -static void odb_source_loose_clear_cache(struct odb_source_loose *loose) -{ - oidtree_clear(loose->cache); - FREE_AND_NULL(loose->cache); - memset(&loose->subdir_seen, 0, - sizeof(loose->subdir_seen)); -} - void odb_source_loose_reprepare(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); @@ -2205,15 +2197,6 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) return &transaction->base; } -void odb_source_loose_free(struct odb_source_loose *loose) -{ - if (!loose) - return; - odb_source_loose_clear_cache(loose); - loose_object_map_clear(&loose->map); - free(loose); -} - struct odb_loose_read_stream { struct odb_read_stream base; git_zstream z; diff --git a/object-file.h b/object-file.h index 1d8312cf7f9ff9..02c9680980ab0f 100644 --- a/object-file.h +++ b/object-file.h @@ -21,8 +21,6 @@ struct object_info; struct odb_read_stream; struct odb_source; -void odb_source_loose_free(struct odb_source_loose *loose); - /* Reprepare the loose source by emptying the loose object cache. */ void odb_source_loose_reprepare(struct odb_source *source); diff --git a/odb/source-files.c b/odb/source-files.c index 185cc6903e35f2..ccc637311b9c21 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -27,7 +27,7 @@ static void odb_source_files_free(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); chdir_notify_unregister(NULL, odb_source_files_reparent, files); - odb_source_loose_free(files->loose); + odb_source_free(&files->loose->base); packfile_store_free(files->packed); odb_source_release(&files->base); free(files); diff --git a/odb/source-loose.c b/odb/source-loose.c index c9e7414814814d..92e18f5adb2b89 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -1,10 +1,55 @@ #include "git-compat-util.h" +#include "abspath.h" +#include "chdir-notify.h" +#include "loose.h" +#include "odb.h" +#include "odb/source-files.h" #include "odb/source-loose.h" +#include "oidtree.h" + +void odb_source_loose_clear_cache(struct odb_source_loose *loose) +{ + oidtree_clear(loose->cache); + FREE_AND_NULL(loose->cache); + memset(&loose->subdir_seen, 0, + sizeof(loose->subdir_seen)); +} + +static void odb_source_loose_reparent(const char *name UNUSED, + const char *old_cwd, + const char *new_cwd, + void *cb_data) +{ + struct odb_source_loose *loose = cb_data; + char *path = reparent_relative_path(old_cwd, new_cwd, + loose->base.path); + free(loose->base.path); + loose->base.path = path; +} + +static void odb_source_loose_free(struct odb_source *source) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + odb_source_loose_clear_cache(loose); + loose_object_map_clear(&loose->map); + chdir_notify_unregister(NULL, odb_source_loose_reparent, loose); + odb_source_release(&loose->base); + free(loose); +} struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) { struct odb_source_loose *loose; + CALLOC_ARRAY(loose, 1); + odb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE, + files->base.path, files->base.local); loose->files = files; + + loose->base.free = odb_source_loose_free; + + if (!is_absolute_path(loose->base.path)) + chdir_notify_register(NULL, odb_source_loose_reparent, loose); + return loose; } diff --git a/odb/source-loose.h b/odb/source-loose.h index bf61e767c8aab4..bd989f0728e622 100644 --- a/odb/source-loose.h +++ b/odb/source-loose.h @@ -12,6 +12,7 @@ struct oidtree; * file per object. This source is part of the files source. */ struct odb_source_loose { + struct odb_source base; struct odb_source_files *files; /* @@ -32,4 +33,17 @@ struct odb_source_loose { struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files); +/* + * Cast the given object database source to the loose backend. This will cause + * a BUG in case the source doesn't use this backend. + */ +static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source) +{ + if (source->type != ODB_SOURCE_LOOSE) + BUG("trying to downcast source of type '%d' to loose", source->type); + return container_of(source, struct odb_source_loose, base); +} + +void odb_source_loose_clear_cache(struct odb_source_loose *loose); + #endif diff --git a/odb/source.h b/odb/source.h index 0a440884e4f0ab..8bcb67787ebafd 100644 --- a/odb/source.h +++ b/odb/source.h @@ -14,6 +14,9 @@ enum odb_source_type { /* The "files" backend that uses loose objects and packfiles. */ ODB_SOURCE_FILES, + /* The "loose" backend that uses loose objects, only. */ + ODB_SOURCE_LOOSE, + /* The "in-memory" backend that stores objects in memory. */ ODB_SOURCE_INMEMORY, }; From a2b7db9bc8d52f133fe8fcb317788d9fe8696f07 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:27 +0200 Subject: [PATCH 024/436] odb/source-loose: wire up `reprepare()` callback Move `odb_source_loose_reprepare()` from "object-file.c" into "odb/source-loose.c" and wire it up as the `reprepare()` callback of the loose source. While at it, make `odb_source_loose_clear_cache()` static, as it is no longer needed outside of its file. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 6 ------ object-file.h | 3 --- odb/source-files.c | 2 +- odb/source-loose.c | 9 ++++++++- odb/source-loose.h | 2 -- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/object-file.c b/object-file.c index 977d959d333166..0f4f1e7bdc0733 100644 --- a/object-file.c +++ b/object-file.c @@ -2041,12 +2041,6 @@ static struct oidtree *odb_source_loose_cache(struct odb_source *source, return files->loose->cache; } -void odb_source_loose_reprepare(struct odb_source *source) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - odb_source_loose_clear_cache(files->loose); -} - static int check_stream_oid(git_zstream *stream, const char *hdr, unsigned long size, diff --git a/object-file.h b/object-file.h index 02c9680980ab0f..420a0fff2e7d7e 100644 --- a/object-file.h +++ b/object-file.h @@ -21,9 +21,6 @@ struct object_info; struct odb_read_stream; struct odb_source; -/* Reprepare the loose source by emptying the loose object cache. */ -void odb_source_loose_reprepare(struct odb_source *source); - int odb_source_loose_read_object_info(struct odb_source *source, const struct object_id *oid, struct object_info *oi, diff --git a/odb/source-files.c b/odb/source-files.c index ccc637311b9c21..10832e81e4e206 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -42,7 +42,7 @@ static void odb_source_files_close(struct odb_source *source) static void odb_source_files_reprepare(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); - odb_source_loose_reprepare(&files->base); + odb_source_reprepare(&files->loose->base); packfile_store_reprepare(files->packed); } diff --git a/odb/source-loose.c b/odb/source-loose.c index 92e18f5adb2b89..e0fe0d513d2532 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -7,7 +7,7 @@ #include "odb/source-loose.h" #include "oidtree.h" -void odb_source_loose_clear_cache(struct odb_source_loose *loose) +static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); FREE_AND_NULL(loose->cache); @@ -15,6 +15,12 @@ void odb_source_loose_clear_cache(struct odb_source_loose *loose) sizeof(loose->subdir_seen)); } +static void odb_source_loose_reprepare(struct odb_source *source) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + odb_source_loose_clear_cache(loose); +} + static void odb_source_loose_reparent(const char *name UNUSED, const char *old_cwd, const char *new_cwd, @@ -47,6 +53,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->files = files; loose->base.free = odb_source_loose_free; + loose->base.reprepare = odb_source_loose_reprepare; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); diff --git a/odb/source-loose.h b/odb/source-loose.h index bd989f0728e622..4dd4fd6ce30a7e 100644 --- a/odb/source-loose.h +++ b/odb/source-loose.h @@ -44,6 +44,4 @@ static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_sour return container_of(source, struct odb_source_loose, base); } -void odb_source_loose_clear_cache(struct odb_source_loose *loose); - #endif From 337b7fccba1cca8b7d9232b5e6e9ff53271f0398 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:28 +0200 Subject: [PATCH 025/436] odb/source-loose: wire up `close()` callback Wire up a new `close()` callback for the loose source and call it from the "files" source via the generic `odb_source_close()` interface. The callback itself is a no-op as the loose source has no resources that need to be released on close. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 1 + odb/source-loose.c | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/odb/source-files.c b/odb/source-files.c index 10832e81e4e206..59e3a70d80d355 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -36,6 +36,7 @@ static void odb_source_files_free(struct odb_source *source) static void odb_source_files_close(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); + odb_source_close(&files->loose->base); packfile_store_close(files->packed); } diff --git a/odb/source-loose.c b/odb/source-loose.c index e0fe0d513d2532..65c1076659b8fd 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -21,6 +21,11 @@ static void odb_source_loose_reprepare(struct odb_source *source) odb_source_loose_clear_cache(loose); } +static void odb_source_loose_close(struct odb_source *source UNUSED) +{ + /* Nothing to do. */ +} + static void odb_source_loose_reparent(const char *name UNUSED, const char *old_cwd, const char *new_cwd, @@ -53,6 +58,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->files = files; loose->base.free = odb_source_loose_free; + loose->base.close = odb_source_loose_close; loose->base.reprepare = odb_source_loose_reprepare; if (!is_absolute_path(loose->base.path)) From 584338ed92735f3be768c16b53266d5bad439a7a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:29 +0200 Subject: [PATCH 026/436] odb/source-loose: wire up `read_object_info()` callback Move `odb_source_loose_read_object_info()` from "object-file.c" into "odb/source-loose.c" and wire it up as the `read_object_info()` callback of the loose source. Callers that previously invoked it directly now go through the generic `odb_source_read_object_info()` interface instead. The function `read_object_info_from_path()` cannot be moved along with it because it is still called by `for_each_object_wrapper_cb()`. It is therefore kept in place, but adjusted to take a loose source to clarify that it's always operating on this structure. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 46 +++++++++++++--------------------------------- object-file.h | 11 ++++++----- odb/source-files.c | 2 +- odb/source-loose.c | 24 ++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/object-file.c b/object-file.c index 0f4f1e7bdc0733..fa174512a43c75 100644 --- a/object-file.c +++ b/object-file.c @@ -396,13 +396,12 @@ static int parse_loose_header(const char *hdr, struct object_info *oi) return 0; } -static int read_object_info_from_path(struct odb_source *source, - const char *path, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags) +int read_object_info_from_path(struct odb_source_loose *loose, + const char *path, + const struct object_id *oid, + struct object_info *oi, + enum object_info_flags flags) { - struct odb_source_files *files = odb_source_files_downcast(source); int ret; int fd; unsigned long mapsize; @@ -425,7 +424,7 @@ static int read_object_info_from_path(struct odb_source *source, struct stat st; if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) { - ret = quick_has_loose(files->loose, oid) ? 0 : -1; + ret = quick_has_loose(loose, oid) ? 0 : -1; goto out; } @@ -532,7 +531,7 @@ static int read_object_info_from_path(struct odb_source *source, if (oi->typep == &type_scratch) oi->typep = NULL; if (oi->delta_base_oid) - oidclr(oi->delta_base_oid, source->odb->repo->hash_algo); + oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo); if (!ret) oi->whence = OI_LOOSE; } @@ -540,26 +539,6 @@ static int read_object_info_from_path(struct odb_source *source, return ret; } -int odb_source_loose_read_object_info(struct odb_source *source, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags) -{ - static struct strbuf buf = STRBUF_INIT; - - /* - * The second read shouldn't cause new loose objects to show up, unless - * there was a race condition with a secondary process. We don't care - * about this case though, so we simply skip reading loose objects a - * second time. - */ - if (flags & OBJECT_INFO_SECOND_READ) - return -1; - - odb_loose_path(source, &buf, oid); - return read_object_info_from_path(source, buf.buf, oid, oi, flags); -} - static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c, const void *buf, unsigned long len, struct object_id *oid, @@ -1833,7 +1812,7 @@ int for_each_loose_file_in_source(struct odb_source *source, } struct for_each_object_wrapper_data { - struct odb_source *source; + struct odb_source_loose *loose; const struct object_info *request; odb_for_each_object_cb cb; void *cb_data; @@ -1848,7 +1827,7 @@ static int for_each_object_wrapper_cb(const struct object_id *oid, if (data->request) { struct object_info oi = *data->request; - if (read_object_info_from_path(data->source, path, oid, &oi, 0) < 0) + if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0) return -1; return data->cb(oid, &oi, data->cb_data); @@ -1865,8 +1844,8 @@ static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid, if (data->request) { struct object_info oi = *data->request; - if (odb_source_loose_read_object_info(data->source, - oid, &oi, 0) < 0) + if (odb_source_read_object_info(&data->loose->base, + oid, &oi, 0) < 0) return -1; return data->cb(oid, &oi, data->cb_data); @@ -1881,8 +1860,9 @@ int odb_source_loose_for_each_object(struct odb_source *source, void *cb_data, const struct odb_for_each_object_options *opts) { + struct odb_source_files *files = odb_source_files_downcast(source); struct for_each_object_wrapper_data data = { - .source = source, + .loose = files->loose, .request = request, .cb = cb, .cb_data = cb_data, diff --git a/object-file.h b/object-file.h index 420a0fff2e7d7e..8ac2832dac3439 100644 --- a/object-file.h +++ b/object-file.h @@ -21,11 +21,6 @@ struct object_info; struct odb_read_stream; struct odb_source; -int odb_source_loose_read_object_info(struct odb_source *source, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags); - int odb_source_loose_read_object_stream(struct odb_read_stream **out, struct odb_source *source, const struct object_id *oid); @@ -198,6 +193,12 @@ int read_loose_object(struct repository *repo, void **contents, struct object_info *oi); +int read_object_info_from_path(struct odb_source_loose *loose, + const char *path, + const struct object_id *oid, + struct object_info *oi, + enum object_info_flags flags); + struct odb_transaction; /* diff --git a/odb/source-files.c b/odb/source-files.c index 59e3a70d80d355..8d6924755ffb70 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -55,7 +55,7 @@ static int odb_source_files_read_object_info(struct odb_source *source, struct odb_source_files *files = odb_source_files_downcast(source); if (!packfile_store_read_object_info(files->packed, oid, oi, flags) || - !odb_source_loose_read_object_info(source, oid, oi, flags)) + !odb_source_read_object_info(&files->loose->base, oid, oi, flags)) return 0; return -1; diff --git a/odb/source-loose.c b/odb/source-loose.c index 65c1076659b8fd..50f387ecf31e38 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -2,10 +2,33 @@ #include "abspath.h" #include "chdir-notify.h" #include "loose.h" +#include "object-file.h" #include "odb.h" #include "odb/source-files.h" #include "odb/source-loose.h" #include "oidtree.h" +#include "strbuf.h" + +static int odb_source_loose_read_object_info(struct odb_source *source, + const struct object_id *oid, + struct object_info *oi, + enum object_info_flags flags) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + static struct strbuf buf = STRBUF_INIT; + + /* + * The second read shouldn't cause new loose objects to show up, unless + * there was a race condition with a secondary process. We don't care + * about this case though, so we simply skip reading loose objects a + * second time. + */ + if (flags & OBJECT_INFO_SECOND_READ) + return -1; + + odb_loose_path(source, &buf, oid); + return read_object_info_from_path(loose, buf.buf, oid, oi, flags); +} static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { @@ -60,6 +83,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.free = odb_source_loose_free; loose->base.close = odb_source_loose_close; loose->base.reprepare = odb_source_loose_reprepare; + loose->base.read_object_info = odb_source_loose_read_object_info; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 727a935a71c29524c936520d8aba4de7098f7566 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:30 +0200 Subject: [PATCH 027/436] odb/source-loose: wire up `read_object_stream()` callback Move `odb_source_loose_read_object_stream()` and its associated helpers from "object-file.c" into "odb/source-loose.c" and wire it up as the `read_object_stream()` callback of the loose source. As part of the move we are also forced to expose a couple of functions from "object-file.h" that parse object headers in a somewhat-generic way, as those functions are now used by both subsystems. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 200 ++------------------------------------------- object-file.h | 31 +++++-- odb/source-files.c | 2 +- odb/source-loose.c | 189 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 200 deletions(-) diff --git a/object-file.c b/object-file.c index fa174512a43c75..adfb6724936452 100644 --- a/object-file.c +++ b/object-file.c @@ -164,28 +164,6 @@ int stream_object_signature(struct repository *r, return !oideq(oid, &real_oid) ? -1 : 0; } -/* - * Find "oid" as a loose object in given source, open the object and return its - * file descriptor. Returns the file descriptor on success, negative on failure. - * - * The "path" out-parameter will give the path of the object we found (if any). - * Note that it may point to static storage and is only valid until another - * call to stat_loose_object(). - */ -static int open_loose_object(struct odb_source_loose *loose, - const struct object_id *oid, const char **path) -{ - static struct strbuf buf = STRBUF_INIT; - int fd; - - *path = odb_loose_path(&loose->files->base, &buf, oid); - fd = git_open(*path); - if (fd >= 0) - return fd; - - return -1; -} - static int quick_has_loose(struct odb_source_loose *loose, const struct object_id *oid) { @@ -215,42 +193,11 @@ static void *map_fd(int fd, const char *path, unsigned long *size) return map; } -static void *odb_source_loose_map_object(struct odb_source *source, - const struct object_id *oid, - unsigned long *size) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - const char *p; - int fd = open_loose_object(files->loose, oid, &p); - - if (fd < 0) - return NULL; - return map_fd(fd, p, size); -} - -enum unpack_loose_header_result { - ULHR_OK, - ULHR_BAD, - ULHR_TOO_LONG, -}; - -/** - * unpack_loose_header() initializes the data stream needed to unpack - * a loose object header. - * - * Returns: - * - * - ULHR_OK on success - * - ULHR_BAD on error - * - ULHR_TOO_LONG if the header was too long - * - * It will only parse up to MAX_HEADER_LEN bytes. - */ -static enum unpack_loose_header_result unpack_loose_header(git_zstream *stream, - unsigned char *map, - unsigned long mapsize, - void *buffer, - unsigned long bufsiz) +enum unpack_loose_header_result unpack_loose_header(git_zstream *stream, + unsigned char *map, + unsigned long mapsize, + void *buffer, + unsigned long bufsiz) { int status; @@ -340,7 +287,7 @@ static void *unpack_loose_rest(git_zstream *stream, * too permissive for what we want to check. So do an anal * object header parse by hand. */ -static int parse_loose_header(const char *hdr, struct object_info *oi) +int parse_loose_header(const char *hdr, struct object_info *oi) { const char *type_buf = hdr; size_t size; @@ -2170,138 +2117,3 @@ struct odb_transaction *odb_transaction_files_begin(struct odb_source *source) return &transaction->base; } - -struct odb_loose_read_stream { - struct odb_read_stream base; - git_zstream z; - enum { - ODB_LOOSE_READ_STREAM_INUSE, - ODB_LOOSE_READ_STREAM_DONE, - ODB_LOOSE_READ_STREAM_ERROR, - } z_state; - void *mapped; - unsigned long mapsize; - char hdr[32]; - int hdr_avail; - int hdr_used; -}; - -static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz) -{ - struct odb_loose_read_stream *st = - container_of(_st, struct odb_loose_read_stream, base); - size_t total_read = 0; - - switch (st->z_state) { - case ODB_LOOSE_READ_STREAM_DONE: - return 0; - case ODB_LOOSE_READ_STREAM_ERROR: - return -1; - default: - break; - } - - if (st->hdr_used < st->hdr_avail) { - size_t to_copy = st->hdr_avail - st->hdr_used; - if (sz < to_copy) - to_copy = sz; - memcpy(buf, st->hdr + st->hdr_used, to_copy); - st->hdr_used += to_copy; - total_read += to_copy; - } - - while (total_read < sz) { - int status; - - st->z.next_out = (unsigned char *)buf + total_read; - st->z.avail_out = sz - total_read; - status = git_inflate(&st->z, Z_FINISH); - - total_read = st->z.next_out - (unsigned char *)buf; - - if (status == Z_STREAM_END) { - git_inflate_end(&st->z); - st->z_state = ODB_LOOSE_READ_STREAM_DONE; - break; - } - if (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) { - git_inflate_end(&st->z); - st->z_state = ODB_LOOSE_READ_STREAM_ERROR; - return -1; - } - } - return total_read; -} - -static int close_istream_loose(struct odb_read_stream *_st) -{ - struct odb_loose_read_stream *st = - container_of(_st, struct odb_loose_read_stream, base); - - if (st->z_state == ODB_LOOSE_READ_STREAM_INUSE) - git_inflate_end(&st->z); - munmap(st->mapped, st->mapsize); - return 0; -} - -int odb_source_loose_read_object_stream(struct odb_read_stream **out, - struct odb_source *source, - const struct object_id *oid) -{ - struct object_info oi = OBJECT_INFO_INIT; - struct odb_loose_read_stream *st; - unsigned long mapsize; - unsigned long size_ul; - void *mapped; - - mapped = odb_source_loose_map_object(source, oid, &mapsize); - if (!mapped) - return -1; - - /* - * Note: we must allocate this structure early even though we may still - * fail. This is because we need to initialize the zlib stream, and it - * is not possible to copy the stream around after the fact because it - * has self-referencing pointers. - */ - CALLOC_ARRAY(st, 1); - - switch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr, - sizeof(st->hdr))) { - case ULHR_OK: - break; - case ULHR_BAD: - case ULHR_TOO_LONG: - goto error; - } - - /* - * object_info.sizep is unsigned long* (32-bit on Windows), but - * st->base.size is size_t (64-bit). Use temporary variable. - * Note: loose objects >4GB would still truncate here, but such - * large loose objects are uncommon (they'd normally be packed). - */ - oi.sizep = &size_ul; - oi.typep = &st->base.type; - - if (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0) - goto error; - st->base.size = size_ul; - - st->mapped = mapped; - st->mapsize = mapsize; - st->hdr_used = strlen(st->hdr) + 1; - st->hdr_avail = st->z.total_out; - st->z_state = ODB_LOOSE_READ_STREAM_INUSE; - st->base.close = close_istream_loose; - st->base.read = read_istream_loose; - - *out = &st->base; - - return 0; -error: - git_inflate_end(&st->z); - munmap(mapped, mapsize); - free(st); - return -1; -} diff --git a/object-file.h b/object-file.h index 8ac2832dac3439..d93b7ffad704b0 100644 --- a/object-file.h +++ b/object-file.h @@ -18,13 +18,8 @@ int index_fd(struct index_state *istate, struct object_id *oid, int fd, struct s int index_path(struct index_state *istate, struct object_id *oid, const char *path, struct stat *st, unsigned flags); struct object_info; -struct odb_read_stream; struct odb_source; -int odb_source_loose_read_object_stream(struct odb_read_stream **out, - struct odb_source *source, - const struct object_id *oid); - /* * Return true iff an object database source has a loose object * with the specified name. This function does not respect replace @@ -199,6 +194,32 @@ int read_object_info_from_path(struct odb_source_loose *loose, struct object_info *oi, enum object_info_flags flags); +enum unpack_loose_header_result { + ULHR_OK, + ULHR_BAD, + ULHR_TOO_LONG, +}; + +/** + * unpack_loose_header() initializes the data stream needed to unpack + * a loose object header. + * + * Returns: + * + * - ULHR_OK on success + * - ULHR_BAD on error + * - ULHR_TOO_LONG if the header was too long + * + * It will only parse up to MAX_HEADER_LEN bytes. + */ +enum unpack_loose_header_result unpack_loose_header(git_zstream *stream, + unsigned char *map, + unsigned long mapsize, + void *buffer, + unsigned long bufsiz); + +int parse_loose_header(const char *hdr, struct object_info *oi); + struct odb_transaction; /* diff --git a/odb/source-files.c b/odb/source-files.c index 8d6924755ffb70..90806ddf86b662 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -67,7 +67,7 @@ static int odb_source_files_read_object_stream(struct odb_read_stream **out, { struct odb_source_files *files = odb_source_files_downcast(source); if (!packfile_store_read_object_stream(out, files->packed, oid) || - !odb_source_loose_read_object_stream(out, source, oid)) + !odb_source_read_object_stream(out, &files->loose->base, oid)) return 0; return -1; } diff --git a/odb/source-loose.c b/odb/source-loose.c index 50f387ecf31e38..4b82c6f316512e 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -1,11 +1,13 @@ #include "git-compat-util.h" #include "abspath.h" #include "chdir-notify.h" +#include "gettext.h" #include "loose.h" #include "object-file.h" #include "odb.h" #include "odb/source-files.h" #include "odb/source-loose.h" +#include "odb/streaming.h" #include "oidtree.h" #include "strbuf.h" @@ -30,6 +32,192 @@ static int odb_source_loose_read_object_info(struct odb_source *source, return read_object_info_from_path(loose, buf.buf, oid, oi, flags); } +/* + * Find "oid" as a loose object in given source, open the object and return its + * file descriptor. Returns the file descriptor on success, negative on failure. + * + * The "path" out-parameter will give the path of the object we found (if any). + * Note that it may point to static storage and is only valid until another + * call to open_loose_object(). + */ +static int open_loose_object(struct odb_source_loose *loose, + const struct object_id *oid, const char **path) +{ + static struct strbuf buf = STRBUF_INIT; + int fd; + + *path = odb_loose_path(&loose->base, &buf, oid); + fd = git_open(*path); + if (fd >= 0) + return fd; + + return -1; +} + +static void *odb_source_loose_map_object(struct odb_source_loose *loose, + const struct object_id *oid, + unsigned long *size) +{ + const char *p; + int fd = open_loose_object(loose, oid, &p); + void *map = NULL; + struct stat st; + + if (fd < 0) + return NULL; + + if (!fstat(fd, &st)) { + *size = xsize_t(st.st_size); + if (!*size) { + /* mmap() is forbidden on empty files */ + error(_("object file %s is empty"), p); + goto out; + } + + map = xmmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0); + } + +out: + close(fd); + return map; +} + +struct odb_loose_read_stream { + struct odb_read_stream base; + git_zstream z; + enum { + ODB_LOOSE_READ_STREAM_INUSE, + ODB_LOOSE_READ_STREAM_DONE, + ODB_LOOSE_READ_STREAM_ERROR, + } z_state; + void *mapped; + unsigned long mapsize; + char hdr[32]; + int hdr_avail; + int hdr_used; +}; + +static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz) +{ + struct odb_loose_read_stream *st = + container_of(_st, struct odb_loose_read_stream, base); + size_t total_read = 0; + + switch (st->z_state) { + case ODB_LOOSE_READ_STREAM_DONE: + return 0; + case ODB_LOOSE_READ_STREAM_ERROR: + return -1; + default: + break; + } + + if (st->hdr_used < st->hdr_avail) { + size_t to_copy = st->hdr_avail - st->hdr_used; + if (sz < to_copy) + to_copy = sz; + memcpy(buf, st->hdr + st->hdr_used, to_copy); + st->hdr_used += to_copy; + total_read += to_copy; + } + + while (total_read < sz) { + int status; + + st->z.next_out = (unsigned char *)buf + total_read; + st->z.avail_out = sz - total_read; + status = git_inflate(&st->z, Z_FINISH); + + total_read = st->z.next_out - (unsigned char *)buf; + + if (status == Z_STREAM_END) { + git_inflate_end(&st->z); + st->z_state = ODB_LOOSE_READ_STREAM_DONE; + break; + } + if (status != Z_OK && (status != Z_BUF_ERROR || total_read < sz)) { + git_inflate_end(&st->z); + st->z_state = ODB_LOOSE_READ_STREAM_ERROR; + return -1; + } + } + return total_read; +} + +static int close_istream_loose(struct odb_read_stream *_st) +{ + struct odb_loose_read_stream *st = + container_of(_st, struct odb_loose_read_stream, base); + + if (st->z_state == ODB_LOOSE_READ_STREAM_INUSE) + git_inflate_end(&st->z); + munmap(st->mapped, st->mapsize); + return 0; +} + +static int odb_source_loose_read_object_stream(struct odb_read_stream **out, + struct odb_source *source, + const struct object_id *oid) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + struct object_info oi = OBJECT_INFO_INIT; + struct odb_loose_read_stream *st; + unsigned long mapsize; + unsigned long size_ul; + void *mapped; + + mapped = odb_source_loose_map_object(loose, oid, &mapsize); + if (!mapped) + return -1; + + /* + * Note: we must allocate this structure early even though we may still + * fail. This is because we need to initialize the zlib stream, and it + * is not possible to copy the stream around after the fact because it + * has self-referencing pointers. + */ + CALLOC_ARRAY(st, 1); + + switch (unpack_loose_header(&st->z, mapped, mapsize, st->hdr, + sizeof(st->hdr))) { + case ULHR_OK: + break; + case ULHR_BAD: + case ULHR_TOO_LONG: + goto error; + } + + /* + * object_info.sizep is unsigned long* (32-bit on Windows), but + * st->base.size is size_t (64-bit). Use temporary variable. + * Note: loose objects >4GB would still truncate here, but such + * large loose objects are uncommon (they'd normally be packed). + */ + oi.sizep = &size_ul; + oi.typep = &st->base.type; + + if (parse_loose_header(st->hdr, &oi) < 0 || st->base.type < 0) + goto error; + st->base.size = size_ul; + + st->mapped = mapped; + st->mapsize = mapsize; + st->hdr_used = strlen(st->hdr) + 1; + st->hdr_avail = st->z.total_out; + st->z_state = ODB_LOOSE_READ_STREAM_INUSE; + st->base.close = close_istream_loose; + st->base.read = read_istream_loose; + + *out = &st->base; + + return 0; +error: + git_inflate_end(&st->z); + munmap(mapped, mapsize); + free(st); + return -1; +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -84,6 +272,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.close = odb_source_loose_close; loose->base.reprepare = odb_source_loose_reprepare; loose->base.read_object_info = odb_source_loose_read_object_info; + loose->base.read_object_stream = odb_source_loose_read_object_stream; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From e4f1d9ba5714957389bee87dd5f9fedb69d8a764 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:31 +0200 Subject: [PATCH 028/436] odb/source-loose: wire up `for_each_object()` callback Move `odb_source_loose_for_each_object()` and its associated helpers from "object-file.c" into "odb/source-loose.c" and wire it up as the `for_each_object()` callback of the loose source. Again, as in the preceding commit, we are forced to expose a couple of functions from "object-file.c" that are now used by both subsystems. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 5 +- object-file.c | 299 +++------------------------------------------ object-file.h | 32 ++--- odb/source-files.c | 2 +- odb/source-loose.c | 264 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 305 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d9fbad535868bb..2958fc53579336 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -862,8 +862,9 @@ static void batch_each_object(struct batch_options *opt, */ odb_prepare_alternates(the_repository->objects); for (source = the_repository->objects->sources; source; source = source->next) { - int ret = odb_source_loose_for_each_object(source, NULL, batch_one_object_oi, - &payload, &opts); + struct odb_source_files *files = odb_source_files_downcast(source); + int ret = odb_source_for_each_object(&files->loose->base, NULL, batch_one_object_oi, + &payload, &opts); if (ret) break; } diff --git a/object-file.c b/object-file.c index adfb6724936452..157ecad3ea204a 100644 --- a/object-file.c +++ b/object-file.c @@ -22,7 +22,6 @@ #include "odb.h" #include "odb/streaming.h" #include "odb/transaction.h" -#include "oidtree.h" #include "pack.h" #include "packfile.h" #include "path.h" @@ -31,12 +30,6 @@ #include "tempfile.h" #include "tmp-objdir.h" -/* The maximum size for an object header. */ -#define MAX_HEADER_LEN 32 - -static struct oidtree *odb_source_loose_cache(struct odb_source *source, - const struct object_id *oid); - static int get_conv_flags(unsigned flags) { if (flags & INDEX_RENORMALIZE) @@ -164,12 +157,6 @@ int stream_object_signature(struct repository *r, return !oideq(oid, &real_oid) ? -1 : 0; } -static int quick_has_loose(struct odb_source_loose *loose, - const struct object_id *oid) -{ - return !!oidtree_contains(odb_source_loose_cache(&loose->files->base, oid), oid); -} - /* * Map and close the given loose object fd. The path argument is used for * error reporting. @@ -227,9 +214,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream, return ULHR_TOO_LONG; } -static void *unpack_loose_rest(git_zstream *stream, - void *buffer, unsigned long size, - const struct object_id *oid) +void *unpack_loose_rest(git_zstream *stream, + void *buffer, unsigned long size, + const struct object_id *oid) { size_t bytes = strlen(buffer) + 1, n; unsigned char *buf = xmallocz(size); @@ -343,149 +330,6 @@ int parse_loose_header(const char *hdr, struct object_info *oi) return 0; } -int read_object_info_from_path(struct odb_source_loose *loose, - const char *path, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags) -{ - int ret; - int fd; - unsigned long mapsize; - void *map = NULL; - git_zstream stream, *stream_to_end = NULL; - char hdr[MAX_HEADER_LEN]; - unsigned long size_scratch; - enum object_type type_scratch; - struct stat st; - - /* - * If we don't care about type or size, then we don't - * need to look inside the object at all. Note that we - * do not optimize out the stat call, even if the - * caller doesn't care about the disk-size, since our - * return value implicitly indicates whether the - * object even exists. - */ - if (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) { - struct stat st; - - if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) { - ret = quick_has_loose(loose, oid) ? 0 : -1; - goto out; - } - - if (lstat(path, &st) < 0) { - ret = -1; - goto out; - } - - if (oi) { - if (oi->disk_sizep) - *oi->disk_sizep = st.st_size; - if (oi->mtimep) - *oi->mtimep = st.st_mtime; - } - - ret = 0; - goto out; - } - - fd = git_open(path); - if (fd < 0) { - if (errno != ENOENT) - error_errno(_("unable to open loose object %s"), oid_to_hex(oid)); - ret = -1; - goto out; - } - - if (fstat(fd, &st)) { - close(fd); - ret = -1; - goto out; - } - - mapsize = xsize_t(st.st_size); - if (!mapsize) { - close(fd); - ret = error(_("object file %s is empty"), path); - goto out; - } - - map = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0); - close(fd); - if (!map) { - ret = -1; - goto out; - } - - if (oi->disk_sizep) - *oi->disk_sizep = mapsize; - if (oi->mtimep) - *oi->mtimep = st.st_mtime; - - stream_to_end = &stream; - - switch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) { - case ULHR_OK: - if (!oi->sizep) - oi->sizep = &size_scratch; - if (!oi->typep) - oi->typep = &type_scratch; - - if (parse_loose_header(hdr, oi) < 0) { - ret = error(_("unable to parse %s header"), oid_to_hex(oid)); - goto corrupt; - } - - if (*oi->typep < 0) - die(_("invalid object type")); - - if (oi->contentp) { - *oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid); - if (!*oi->contentp) { - ret = -1; - goto corrupt; - } - } - - break; - case ULHR_BAD: - ret = error(_("unable to unpack %s header"), - oid_to_hex(oid)); - goto corrupt; - case ULHR_TOO_LONG: - ret = error(_("header for %s too long, exceeds %d bytes"), - oid_to_hex(oid), MAX_HEADER_LEN); - goto corrupt; - } - - ret = 0; - -corrupt: - if (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT)) - die(_("loose object %s (stored in %s) is corrupt"), - oid_to_hex(oid), path); - -out: - if (stream_to_end) - git_inflate_end(stream_to_end); - if (map) - munmap(map, mapsize); - if (oi) { - if (oi->sizep == &size_scratch) - oi->sizep = NULL; - if (oi->typep == &type_scratch) - oi->typep = NULL; - if (oi->delta_base_oid) - oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo); - if (!ret) - oi->whence = OI_LOOSE; - } - - return ret; -} - static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_ctx *c, const void *buf, unsigned long len, struct object_id *oid, @@ -1667,13 +1511,13 @@ int read_pack_header(int fd, struct pack_header *header) return 0; } -static int for_each_file_in_obj_subdir(unsigned int subdir_nr, - struct strbuf *path, - const struct git_hash_algo *algop, - each_loose_object_fn obj_cb, - each_loose_cruft_fn cruft_cb, - each_loose_subdir_fn subdir_cb, - void *data) +int for_each_file_in_obj_subdir(unsigned int subdir_nr, + struct strbuf *path, + const struct git_hash_algo *algop, + each_loose_object_fn obj_cb, + each_loose_cruft_fn cruft_cb, + each_loose_subdir_fn subdir_cb, + void *data) { size_t origlen, baselen; DIR *dir; @@ -1758,78 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source, return r; } -struct for_each_object_wrapper_data { - struct odb_source_loose *loose; - const struct object_info *request; - odb_for_each_object_cb cb; - void *cb_data; -}; - -static int for_each_object_wrapper_cb(const struct object_id *oid, - const char *path, - void *cb_data) -{ - struct for_each_object_wrapper_data *data = cb_data; - - if (data->request) { - struct object_info oi = *data->request; - - if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0) - return -1; - - return data->cb(oid, &oi, data->cb_data); - } else { - return data->cb(oid, NULL, data->cb_data); - } -} - -static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid, - void *node_data UNUSED, - void *cb_data) -{ - struct for_each_object_wrapper_data *data = cb_data; - if (data->request) { - struct object_info oi = *data->request; - - if (odb_source_read_object_info(&data->loose->base, - oid, &oi, 0) < 0) - return -1; - - return data->cb(oid, &oi, data->cb_data); - } else { - return data->cb(oid, NULL, data->cb_data); - } -} - -int odb_source_loose_for_each_object(struct odb_source *source, - const struct object_info *request, - odb_for_each_object_cb cb, - void *cb_data, - const struct odb_for_each_object_options *opts) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - struct for_each_object_wrapper_data data = { - .loose = files->loose, - .request = request, - .cb = cb, - .cb_data = cb_data, - }; - - /* There are no loose promisor objects, so we can return immediately. */ - if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) - return 0; - if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local) - return 0; - - if (opts->prefix) - return oidtree_each(odb_source_loose_cache(source, opts->prefix), - opts->prefix, opts->prefix_hex_len, - for_each_prefixed_object_wrapper_cb, &data); - - return for_each_loose_file_in_source(source, for_each_object_wrapper_cb, - NULL, NULL, &data); -} - static int count_loose_object(const struct object_id *oid UNUSED, struct object_info *oi UNUSED, void *payload) @@ -1843,6 +1615,7 @@ int odb_source_loose_count_objects(struct odb_source *source, enum odb_count_objects_flags flags, unsigned long *out) { + struct odb_source_files *files = odb_source_files_downcast(source); const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2; char *path = NULL; DIR *dir = NULL; @@ -1878,8 +1651,8 @@ int odb_source_loose_count_objects(struct odb_source *source, } else { struct odb_for_each_object_options opts = { 0 }; *out = 0; - ret = odb_source_loose_for_each_object(source, NULL, count_loose_object, - out, &opts); + ret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object, + out, &opts); } out: @@ -1910,6 +1683,7 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source, unsigned min_len, unsigned *out) { + struct odb_source_files *files = odb_source_files_downcast(source); struct odb_for_each_object_options opts = { .prefix = oid, .prefix_hex_len = min_len, @@ -1920,54 +1694,13 @@ int odb_source_loose_find_abbrev_len(struct odb_source *source, }; int ret; - ret = odb_source_loose_for_each_object(source, NULL, find_abbrev_len_cb, - &data, &opts); + ret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb, + &data, &opts); *out = data.len; return ret; } -static int append_loose_object(const struct object_id *oid, - const char *path UNUSED, - void *data) -{ - oidtree_insert(data, oid, NULL); - return 0; -} - -static struct oidtree *odb_source_loose_cache(struct odb_source *source, - const struct object_id *oid) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - int subdir_nr = oid->hash[0]; - struct strbuf buf = STRBUF_INIT; - size_t word_bits = bitsizeof(files->loose->subdir_seen[0]); - size_t word_index = subdir_nr / word_bits; - size_t mask = (size_t)1u << (subdir_nr % word_bits); - uint32_t *bitmap; - - if (subdir_nr < 0 || - (size_t) subdir_nr >= bitsizeof(files->loose->subdir_seen)) - BUG("subdir_nr out of range"); - - bitmap = &files->loose->subdir_seen[word_index]; - if (*bitmap & mask) - return files->loose->cache; - if (!files->loose->cache) { - ALLOC_ARRAY(files->loose->cache, 1); - oidtree_init(files->loose->cache); - } - strbuf_addstr(&buf, source->path); - for_each_file_in_obj_subdir(subdir_nr, &buf, - source->odb->repo->hash_algo, - append_loose_object, - NULL, NULL, - files->loose->cache); - *bitmap |= mask; - strbuf_release(&buf); - return files->loose->cache; -} - static int check_stream_oid(git_zstream *stream, const char *hdr, unsigned long size, diff --git a/object-file.h b/object-file.h index d93b7ffad704b0..9ee5649220931b 100644 --- a/object-file.h +++ b/object-file.h @@ -6,6 +6,9 @@ #include "odb.h" #include "odb/source-loose.h" +/* The maximum size for an object header. */ +#define MAX_HEADER_LEN 32 + struct index_state; enum { @@ -85,19 +88,13 @@ int for_each_loose_file_in_source(struct odb_source *source, each_loose_cruft_fn cruft_cb, each_loose_subdir_fn subdir_cb, void *data); - -/* - * Iterate through all loose objects in the given object database source and - * invoke the callback function for each of them. If an object info request is - * given, then the object info will be read for every individual object and - * passed to the callback as if `odb_source_loose_read_object_info()` was - * called for the object. - */ -int odb_source_loose_for_each_object(struct odb_source *source, - const struct object_info *request, - odb_for_each_object_cb cb, - void *cb_data, - const struct odb_for_each_object_options *opts); +int for_each_file_in_obj_subdir(unsigned int subdir_nr, + struct strbuf *path, + const struct git_hash_algo *algop, + each_loose_object_fn obj_cb, + each_loose_cruft_fn cruft_cb, + each_loose_subdir_fn subdir_cb, + void *data); /* * Count the number of loose objects in this source. @@ -188,12 +185,6 @@ int read_loose_object(struct repository *repo, void **contents, struct object_info *oi); -int read_object_info_from_path(struct odb_source_loose *loose, - const char *path, - const struct object_id *oid, - struct object_info *oi, - enum object_info_flags flags); - enum unpack_loose_header_result { ULHR_OK, ULHR_BAD, @@ -217,6 +208,9 @@ enum unpack_loose_header_result unpack_loose_header(git_zstream *stream, unsigned long mapsize, void *buffer, unsigned long bufsiz); +void *unpack_loose_rest(git_zstream *stream, + void *buffer, unsigned long size, + const struct object_id *oid); int parse_loose_header(const char *hdr, struct object_info *oi); diff --git a/odb/source-files.c b/odb/source-files.c index 90806ddf86b662..676a641739bcbf 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -82,7 +82,7 @@ static int odb_source_files_for_each_object(struct odb_source *source, int ret; if (!(opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) { - ret = odb_source_loose_for_each_object(source, request, cb, cb_data, opts); + ret = odb_source_for_each_object(&files->loose->base, request, cb, cb_data, opts); if (ret) return ret; } diff --git a/odb/source-loose.c b/odb/source-loose.c index 4b82c6f316512e..4e8b923498b5b2 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -2,6 +2,7 @@ #include "abspath.h" #include "chdir-notify.h" #include "gettext.h" +#include "hex.h" #include "loose.h" #include "object-file.h" #include "odb.h" @@ -9,8 +10,198 @@ #include "odb/source-loose.h" #include "odb/streaming.h" #include "oidtree.h" +#include "repository.h" #include "strbuf.h" +static int append_loose_object(const struct object_id *oid, + const char *path UNUSED, + void *data) +{ + oidtree_insert(data, oid, NULL); + return 0; +} + +static struct oidtree *odb_source_loose_cache(struct odb_source_loose *loose, + const struct object_id *oid) +{ + int subdir_nr = oid->hash[0]; + struct strbuf buf = STRBUF_INIT; + size_t word_bits = bitsizeof(loose->subdir_seen[0]); + size_t word_index = subdir_nr / word_bits; + size_t mask = (size_t)1u << (subdir_nr % word_bits); + uint32_t *bitmap; + + if (subdir_nr < 0 || + (size_t) subdir_nr >= bitsizeof(loose->subdir_seen)) + BUG("subdir_nr out of range"); + + bitmap = &loose->subdir_seen[word_index]; + if (*bitmap & mask) + return loose->cache; + if (!loose->cache) { + ALLOC_ARRAY(loose->cache, 1); + oidtree_init(loose->cache); + } + strbuf_addstr(&buf, loose->base.path); + for_each_file_in_obj_subdir(subdir_nr, &buf, + loose->base.odb->repo->hash_algo, + append_loose_object, + NULL, NULL, + loose->cache); + *bitmap |= mask; + strbuf_release(&buf); + return loose->cache; +} + +static int quick_has_loose(struct odb_source_loose *loose, + const struct object_id *oid) +{ + return !!oidtree_contains(odb_source_loose_cache(loose, oid), oid); +} + +static int read_object_info_from_path(struct odb_source_loose *loose, + const char *path, + const struct object_id *oid, + struct object_info *oi, + enum object_info_flags flags) +{ + int ret; + int fd; + unsigned long mapsize; + void *map = NULL; + git_zstream stream, *stream_to_end = NULL; + char hdr[MAX_HEADER_LEN]; + unsigned long size_scratch; + enum object_type type_scratch; + struct stat st; + + /* + * If we don't care about type or size, then we don't + * need to look inside the object at all. Note that we + * do not optimize out the stat call, even if the + * caller doesn't care about the disk-size, since our + * return value implicitly indicates whether the + * object even exists. + */ + if (!oi || (!oi->typep && !oi->sizep && !oi->contentp)) { + struct stat st; + + if ((!oi || (!oi->disk_sizep && !oi->mtimep)) && (flags & OBJECT_INFO_QUICK)) { + ret = quick_has_loose(loose, oid) ? 0 : -1; + goto out; + } + + if (lstat(path, &st) < 0) { + ret = -1; + goto out; + } + + if (oi) { + if (oi->disk_sizep) + *oi->disk_sizep = st.st_size; + if (oi->mtimep) + *oi->mtimep = st.st_mtime; + } + + ret = 0; + goto out; + } + + fd = git_open(path); + if (fd < 0) { + if (errno != ENOENT) + error_errno(_("unable to open loose object %s"), oid_to_hex(oid)); + ret = -1; + goto out; + } + + if (fstat(fd, &st)) { + close(fd); + ret = -1; + goto out; + } + + mapsize = xsize_t(st.st_size); + if (!mapsize) { + close(fd); + ret = error(_("object file %s is empty"), path); + goto out; + } + + map = xmmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (!map) { + ret = -1; + goto out; + } + + if (oi->disk_sizep) + *oi->disk_sizep = mapsize; + if (oi->mtimep) + *oi->mtimep = st.st_mtime; + + stream_to_end = &stream; + + switch (unpack_loose_header(&stream, map, mapsize, hdr, sizeof(hdr))) { + case ULHR_OK: + if (!oi->sizep) + oi->sizep = &size_scratch; + if (!oi->typep) + oi->typep = &type_scratch; + + if (parse_loose_header(hdr, oi) < 0) { + ret = error(_("unable to parse %s header"), oid_to_hex(oid)); + goto corrupt; + } + + if (*oi->typep < 0) + die(_("invalid object type")); + + if (oi->contentp) { + *oi->contentp = unpack_loose_rest(&stream, hdr, *oi->sizep, oid); + if (!*oi->contentp) { + ret = -1; + goto corrupt; + } + } + + break; + case ULHR_BAD: + ret = error(_("unable to unpack %s header"), + oid_to_hex(oid)); + goto corrupt; + case ULHR_TOO_LONG: + ret = error(_("header for %s too long, exceeds %d bytes"), + oid_to_hex(oid), MAX_HEADER_LEN); + goto corrupt; + } + + ret = 0; + +corrupt: + if (ret && (flags & OBJECT_INFO_DIE_IF_CORRUPT)) + die(_("loose object %s (stored in %s) is corrupt"), + oid_to_hex(oid), path); + +out: + if (stream_to_end) + git_inflate_end(stream_to_end); + if (map) + munmap(map, mapsize); + if (oi) { + if (oi->sizep == &size_scratch) + oi->sizep = NULL; + if (oi->typep == &type_scratch) + oi->typep = NULL; + if (oi->delta_base_oid) + oidclr(oi->delta_base_oid, loose->base.odb->repo->hash_algo); + if (!ret) + oi->whence = OI_LOOSE; + } + + return ret; +} + static int odb_source_loose_read_object_info(struct odb_source *source, const struct object_id *oid, struct object_info *oi, @@ -218,6 +409,78 @@ static int odb_source_loose_read_object_stream(struct odb_read_stream **out, return -1; } +struct for_each_object_wrapper_data { + struct odb_source_loose *loose; + const struct object_info *request; + odb_for_each_object_cb cb; + void *cb_data; +}; + +static int for_each_object_wrapper_cb(const struct object_id *oid, + const char *path, + void *cb_data) +{ + struct for_each_object_wrapper_data *data = cb_data; + + if (data->request) { + struct object_info oi = *data->request; + + if (read_object_info_from_path(data->loose, path, oid, &oi, 0) < 0) + return -1; + + return data->cb(oid, &oi, data->cb_data); + } else { + return data->cb(oid, NULL, data->cb_data); + } +} + +static int for_each_prefixed_object_wrapper_cb(const struct object_id *oid, + void *node_data UNUSED, + void *cb_data) +{ + struct for_each_object_wrapper_data *data = cb_data; + if (data->request) { + struct object_info oi = *data->request; + + if (odb_source_read_object_info(&data->loose->base, + oid, &oi, 0) < 0) + return -1; + + return data->cb(oid, &oi, data->cb_data); + } else { + return data->cb(oid, NULL, data->cb_data); + } +} + +static int odb_source_loose_for_each_object(struct odb_source *source, + const struct object_info *request, + odb_for_each_object_cb cb, + void *cb_data, + const struct odb_for_each_object_options *opts) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + struct for_each_object_wrapper_data data = { + .loose = loose, + .request = request, + .cb = cb, + .cb_data = cb_data, + }; + + /* There are no loose promisor objects, so we can return immediately. */ + if ((opts->flags & ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)) + return 0; + if ((opts->flags & ODB_FOR_EACH_OBJECT_LOCAL_ONLY) && !source->local) + return 0; + + if (opts->prefix) + return oidtree_each(odb_source_loose_cache(loose, opts->prefix), + opts->prefix, opts->prefix_hex_len, + for_each_prefixed_object_wrapper_cb, &data); + + return for_each_loose_file_in_source(source, for_each_object_wrapper_cb, + NULL, NULL, &data); +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -273,6 +536,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.reprepare = odb_source_loose_reprepare; loose->base.read_object_info = odb_source_loose_read_object_info; loose->base.read_object_stream = odb_source_loose_read_object_stream; + loose->base.for_each_object = odb_source_loose_for_each_object; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 8a6da81cc113607bdc1ac08395f6e7121cd652e9 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:32 +0200 Subject: [PATCH 029/436] odb/source-loose: wire up `find_abbrev_len()` callback Move `odb_source_loose_find_abbrev_len()` and its associated helpers from "object-file.c" into "odb/source-loose.c" and wire it up as the `find_abbrev_len` callback of the loose source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 39 --------------------------------------- object-file.h | 12 ------------ odb/source-files.c | 2 +- odb/source-loose.c | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 52 deletions(-) diff --git a/object-file.c b/object-file.c index 157ecad3ea204a..11957aa44f44fd 100644 --- a/object-file.c +++ b/object-file.c @@ -1662,45 +1662,6 @@ int odb_source_loose_count_objects(struct odb_source *source, return ret; } -struct find_abbrev_len_data { - const struct object_id *oid; - unsigned len; -}; - -static int find_abbrev_len_cb(const struct object_id *oid, - struct object_info *oi UNUSED, - void *cb_data) -{ - struct find_abbrev_len_data *data = cb_data; - unsigned len = oid_common_prefix_hexlen(oid, data->oid); - if (len != hash_algos[oid->algo].hexsz && len >= data->len) - data->len = len + 1; - return 0; -} - -int odb_source_loose_find_abbrev_len(struct odb_source *source, - const struct object_id *oid, - unsigned min_len, - unsigned *out) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - struct odb_for_each_object_options opts = { - .prefix = oid, - .prefix_hex_len = min_len, - }; - struct find_abbrev_len_data data = { - .oid = oid, - .len = min_len, - }; - int ret; - - ret = odb_source_for_each_object(&files->loose->base, NULL, find_abbrev_len_cb, - &data, &opts); - *out = data.len; - - return ret; -} - static int check_stream_oid(git_zstream *stream, const char *hdr, unsigned long size, diff --git a/object-file.h b/object-file.h index 9ee5649220931b..96760db0e1cb2b 100644 --- a/object-file.h +++ b/object-file.h @@ -110,18 +110,6 @@ int odb_source_loose_count_objects(struct odb_source *source, enum odb_count_objects_flags flags, unsigned long *out); -/* - * Find the shortest unique prefix for the given object ID, where `min_len` is - * the minimum length that the prefix should have. - * - * Returns 0 on success, in which case the computed length will be written to - * `out`. Otherwise, a negative error code is returned. - */ -int odb_source_loose_find_abbrev_len(struct odb_source *source, - const struct object_id *oid, - unsigned min_len, - unsigned *out); - /** * format_object_header() is a thin wrapper around s xsnprintf() that * writes the initial " " part of the loose object diff --git a/odb/source-files.c b/odb/source-files.c index 676a641739bcbf..4a54b10e4af11d 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -136,7 +136,7 @@ static int odb_source_files_find_abbrev_len(struct odb_source *source, if (ret < 0) goto out; - ret = odb_source_loose_find_abbrev_len(source, oid, len, &len); + ret = odb_source_find_abbrev_len(&files->loose->base, oid, len, &len); if (ret < 0) goto out; diff --git a/odb/source-loose.c b/odb/source-loose.c index 4e8b923498b5b2..4b8d10bc870374 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -481,6 +481,45 @@ static int odb_source_loose_for_each_object(struct odb_source *source, NULL, NULL, &data); } +struct find_abbrev_len_data { + const struct object_id *oid; + unsigned len; +}; + +static int find_abbrev_len_cb(const struct object_id *oid, + struct object_info *oi UNUSED, + void *cb_data) +{ + struct find_abbrev_len_data *data = cb_data; + unsigned len = oid_common_prefix_hexlen(oid, data->oid); + if (len != hash_algos[oid->algo].hexsz && len >= data->len) + data->len = len + 1; + return 0; +} + +static int odb_source_loose_find_abbrev_len(struct odb_source *source, + const struct object_id *oid, + unsigned min_len, + unsigned *out) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + struct odb_for_each_object_options opts = { + .prefix = oid, + .prefix_hex_len = min_len, + }; + struct find_abbrev_len_data data = { + .oid = oid, + .len = min_len, + }; + int ret; + + ret = odb_source_for_each_object(&loose->base, NULL, find_abbrev_len_cb, + &data, &opts); + *out = data.len; + + return ret; +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -537,6 +576,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.read_object_info = odb_source_loose_read_object_info; loose->base.read_object_stream = odb_source_loose_read_object_stream; loose->base.for_each_object = odb_source_loose_for_each_object; + loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 2ade08ac2978dc1c908602c2a4d653836ecb5acb Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:33 +0200 Subject: [PATCH 030/436] odb/source-loose: wire up `count_objects()` callback Move `odb_source_loose_count_objects()` and its associated helpers from "object-file.c" into "odb/source-loose.c" and wire it up as the `count_objects()` callback of the loose source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/gc.c | 6 ++--- object-file.c | 60 --------------------------------------------- object-file.h | 14 ----------- odb/source-files.c | 2 +- odb/source-loose.c | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 78 deletions(-) diff --git a/builtin/gc.c b/builtin/gc.c index 84a66d32404e4d..c26c93ee0fe4a3 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -466,6 +466,7 @@ static int rerere_gc_condition(struct gc_config *cfg UNUSED) static int too_many_loose_objects(int limit) { + struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); /* * This is weird, but stems from legacy behaviour: the GC auto * threshold was always essentially interpreted as if it was rounded up @@ -474,9 +475,8 @@ static int too_many_loose_objects(int limit) int auto_threshold = DIV_ROUND_UP(limit, 256) * 256; unsigned long loose_count; - if (odb_source_loose_count_objects(the_repository->objects->sources, - ODB_COUNT_OBJECTS_APPROXIMATE, - &loose_count) < 0) + if (odb_source_count_objects(&files->loose->base, ODB_COUNT_OBJECTS_APPROXIMATE, + &loose_count) < 0) return 0; return loose_count > auto_threshold; diff --git a/object-file.c b/object-file.c index 11957aa44f44fd..9b2044de3784e6 100644 --- a/object-file.c +++ b/object-file.c @@ -1602,66 +1602,6 @@ int for_each_loose_file_in_source(struct odb_source *source, return r; } -static int count_loose_object(const struct object_id *oid UNUSED, - struct object_info *oi UNUSED, - void *payload) -{ - unsigned long *count = payload; - (*count)++; - return 0; -} - -int odb_source_loose_count_objects(struct odb_source *source, - enum odb_count_objects_flags flags, - unsigned long *out) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2; - char *path = NULL; - DIR *dir = NULL; - int ret; - - if (flags & ODB_COUNT_OBJECTS_APPROXIMATE) { - unsigned long count = 0; - struct dirent *ent; - - path = xstrfmt("%s/17", source->path); - - dir = opendir(path); - if (!dir) { - if (errno == ENOENT) { - *out = 0; - ret = 0; - goto out; - } - - ret = error_errno("cannot open object shard '%s'", path); - goto out; - } - - while ((ent = readdir(dir)) != NULL) { - if (strspn(ent->d_name, "0123456789abcdef") != hexsz || - ent->d_name[hexsz] != '\0') - continue; - count++; - } - - *out = count * 256; - ret = 0; - } else { - struct odb_for_each_object_options opts = { 0 }; - *out = 0; - ret = odb_source_for_each_object(&files->loose->base, NULL, count_loose_object, - out, &opts); - } - -out: - if (dir) - closedir(dir); - free(path); - return ret; -} - static int check_stream_oid(git_zstream *stream, const char *hdr, unsigned long size, diff --git a/object-file.h b/object-file.h index 96760db0e1cb2b..bc72d89f548915 100644 --- a/object-file.h +++ b/object-file.h @@ -96,20 +96,6 @@ int for_each_file_in_obj_subdir(unsigned int subdir_nr, each_loose_subdir_fn subdir_cb, void *data); -/* - * Count the number of loose objects in this source. - * - * The object count is approximated by opening a single sharding directory for - * loose objects and scanning its contents. The result is then extrapolated by - * 256. This should generally work as a reasonable estimate given that the - * object hash is supposed to be indistinguishable from random. - * - * Returns 0 on success, a negative error code otherwise. - */ -int odb_source_loose_count_objects(struct odb_source *source, - enum odb_count_objects_flags flags, - unsigned long *out); - /** * format_object_header() is a thin wrapper around s xsnprintf() that * writes the initial " " part of the loose object diff --git a/odb/source-files.c b/odb/source-files.c index 4a54b10e4af11d..d5454e170dee66 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -109,7 +109,7 @@ static int odb_source_files_count_objects(struct odb_source *source, if (!(flags & ODB_COUNT_OBJECTS_APPROXIMATE)) { unsigned long loose_count; - ret = odb_source_loose_count_objects(source, flags, &loose_count); + ret = odb_source_count_objects(&files->loose->base, flags, &loose_count); if (ret < 0) goto out; diff --git a/odb/source-loose.c b/odb/source-loose.c index 4b8d10bc870374..27be066327a313 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -520,6 +520,66 @@ static int odb_source_loose_find_abbrev_len(struct odb_source *source, return ret; } +static int count_loose_object(const struct object_id *oid UNUSED, + struct object_info *oi UNUSED, + void *payload) +{ + unsigned long *count = payload; + (*count)++; + return 0; +} + +static int odb_source_loose_count_objects(struct odb_source *source, + enum odb_count_objects_flags flags, + unsigned long *out) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + const unsigned hexsz = source->odb->repo->hash_algo->hexsz - 2; + char *path = NULL; + DIR *dir = NULL; + int ret; + + if (flags & ODB_COUNT_OBJECTS_APPROXIMATE) { + unsigned long count = 0; + struct dirent *ent; + + path = xstrfmt("%s/17", source->path); + + dir = opendir(path); + if (!dir) { + if (errno == ENOENT) { + *out = 0; + ret = 0; + goto out; + } + + ret = error_errno("cannot open object shard '%s'", path); + goto out; + } + + while ((ent = readdir(dir)) != NULL) { + if (strspn(ent->d_name, "0123456789abcdef") != hexsz || + ent->d_name[hexsz] != '\0') + continue; + count++; + } + + *out = count * 256; + ret = 0; + } else { + struct odb_for_each_object_options opts = { 0 }; + *out = 0; + ret = odb_source_for_each_object(&loose->base, NULL, count_loose_object, + out, &opts); + } + +out: + if (dir) + closedir(dir); + free(path); + return ret; +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -577,6 +637,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.read_object_stream = odb_source_loose_read_object_stream; loose->base.for_each_object = odb_source_loose_for_each_object; loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len; + loose->base.count_objects = odb_source_loose_count_objects; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 86f7ab5a1f12ecfdf51b6df0b9b014e2329944be Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:34 +0200 Subject: [PATCH 031/436] odb/source-loose: drop `odb_source_loose_has_object()` The function `odb_source_loose_has_object()` checks whether a specific object exists as a loose object on disk by using lstat(3p). This interface is somewhat redundant, as we typically check for object existence in a generic way via `odb_source_read_object_info()`. In fact, these two calls are redundant in case the latter is called in a specific way: when called without an object info request and without the `OBJECT_INFO_QUICK` flag, then we will end up doing the same call to lstat(3p) in `read_object_info_from_path()`. Drop the function and adapt callers to instead use the generic interface so that its calling conventions align with that of other sources. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- builtin/pack-objects.c | 12 ++++++++---- object-file.c | 12 ++++-------- object-file.h | 8 -------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 480cc0bd8c8d22..a6be3d659f8e36 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1750,9 +1750,11 @@ static int want_object_in_pack_mtime(const struct object_id *oid, * skip the local object source. */ struct odb_source *source = the_repository->objects->sources->next; - for (; source; source = source->next) - if (odb_source_loose_has_object(source, oid)) + for (; source; source = source->next) { + struct odb_source_files *files = odb_source_files_downcast(source); + if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0)) return 0; + } } /* @@ -4135,9 +4137,11 @@ static void add_cruft_object_entry(const struct object_id *oid, enum object_type struct odb_source *source = the_repository->objects->sources; int found = 0; - for (; !found && source; source = source->next) - if (odb_source_loose_has_object(source, oid)) + for (; !found && source; source = source->next) { + struct odb_source_files *files = odb_source_files_downcast(source); + if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0)) found = 1; + } /* * If a traversed tree has a missing blob then we want diff --git a/object-file.c b/object-file.c index 9b2044de3784e6..c83136cf70024c 100644 --- a/object-file.c +++ b/object-file.c @@ -96,12 +96,6 @@ static int check_and_freshen_source(struct odb_source *source, return check_and_freshen_file(path.buf, freshen); } -int odb_source_loose_has_object(struct odb_source *source, - const struct object_id *oid) -{ - return check_and_freshen_source(source, oid, 0); -} - int format_object_header(char *str, size_t size, enum object_type type, size_t objsize) { @@ -1000,9 +994,11 @@ int force_object_loose(struct odb_source *source, int hdrlen; int ret; - for (struct odb_source *s = source->odb->sources; s; s = s->next) - if (odb_source_loose_has_object(s, oid)) + for (struct odb_source *s = source->odb->sources; s; s = s->next) { + struct odb_source_files *files = odb_source_files_downcast(s); + if (!odb_source_read_object_info(&files->loose->base, oid, NULL, 0)) return 0; + } oi.typep = &type; oi.sizep = &len; diff --git a/object-file.h b/object-file.h index bc72d89f548915..506ca6be40b749 100644 --- a/object-file.h +++ b/object-file.h @@ -23,14 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -/* - * Return true iff an object database source has a loose object - * with the specified name. This function does not respect replace - * references. - */ -int odb_source_loose_has_object(struct odb_source *source, - const struct object_id *oid); - int odb_source_loose_freshen_object(struct odb_source *source, const struct object_id *oid); From d8b9e8bb23ece128179ad54ed5ecbcd4bd809b1e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:35 +0200 Subject: [PATCH 032/436] odb/source-loose: wire up `freshen_object()` callback Move `odb_source_loose_freshen_object()` from "object-file.c" into "odb/source-loose.c" and wire it up as the `freshen_object()` callback of the loose source. As part of the move, `check_and_freshen_source()` is inlined into the callback function, as it has no other callers anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 15 --------------- object-file.h | 3 --- odb/source-files.c | 2 +- odb/source-loose.c | 9 +++++++++ 4 files changed, 10 insertions(+), 19 deletions(-) diff --git a/object-file.c b/object-file.c index c83136cf70024c..0689a4e67b156a 100644 --- a/object-file.c +++ b/object-file.c @@ -87,15 +87,6 @@ int check_and_freshen_file(const char *fn, int freshen) return 1; } -static int check_and_freshen_source(struct odb_source *source, - const struct object_id *oid, - int freshen) -{ - static struct strbuf path = STRBUF_INIT; - odb_loose_path(source, &path, oid); - return check_and_freshen_file(path.buf, freshen); -} - int format_object_header(char *str, size_t size, enum object_type type, size_t objsize) { @@ -815,12 +806,6 @@ static int write_loose_object(struct odb_source *source, FOF_SKIP_COLLISION_CHECK); } -int odb_source_loose_freshen_object(struct odb_source *source, - const struct object_id *oid) -{ - return !!check_and_freshen_source(source, oid, 1); -} - int odb_source_loose_write_stream(struct odb_source *source, struct odb_write_stream *in_stream, size_t len, struct object_id *oid) diff --git a/object-file.h b/object-file.h index 506ca6be40b749..1d90df9d98b78e 100644 --- a/object-file.h +++ b/object-file.h @@ -23,9 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -int odb_source_loose_freshen_object(struct odb_source *source, - const struct object_id *oid); - int odb_source_loose_write_object(struct odb_source *source, const void *buf, unsigned long len, enum object_type type, struct object_id *oid, diff --git a/odb/source-files.c b/odb/source-files.c index d5454e170dee66..ef548e6fe69cd0 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -152,7 +152,7 @@ static int odb_source_files_freshen_object(struct odb_source *source, { struct odb_source_files *files = odb_source_files_downcast(source); if (packfile_store_freshen_object(files->packed, oid) || - odb_source_loose_freshen_object(source, oid)) + odb_source_freshen_object(&files->loose->base, oid)) return 1; return 0; } diff --git a/odb/source-loose.c b/odb/source-loose.c index 27be066327a313..e519365d23f680 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -580,6 +580,14 @@ static int odb_source_loose_count_objects(struct odb_source *source, return ret; } +static int odb_source_loose_freshen_object(struct odb_source *source, + const struct object_id *oid) +{ + static struct strbuf path = STRBUF_INIT; + odb_loose_path(source, &path, oid); + return !!check_and_freshen_file(path.buf, 1); +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -638,6 +646,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.for_each_object = odb_source_loose_for_each_object; loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len; loose->base.count_objects = odb_source_loose_count_objects; + loose->base.freshen_object = odb_source_loose_freshen_object; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 87588db131a5c1c33471606860951c9959bbe6ae Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:36 +0200 Subject: [PATCH 033/436] loose: refactor object map to operate on `struct odb_source_loose` While the loose object map functions in "loose.c" accept a generic `struct odb_source *`, they always expect this to be the "files" backend. Furthermore, the subsystem doesn't even care about the "files" backend, but only uses it as a stepping stone to get to the "loose" backend. This assumption is implicit and thus not immediately obvious. Refactor the interfaces to instead operate on a `struct odb_source_loose` instead, which eliminates the implicit dependency and unnecessary detour via the "files" source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- loose.c | 45 ++++++++++++++++++++++----------------------- loose.h | 4 ++-- object-file.c | 9 ++++++--- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/loose.c b/loose.c index f7a3dd1a72f0fc..0b626c1b854642 100644 --- a/loose.c +++ b/loose.c @@ -46,38 +46,36 @@ static int insert_oid_pair(kh_oid_map_t *map, const struct object_id *key, const return 1; } -static int insert_loose_map(struct odb_source *source, +static int insert_loose_map(struct odb_source_loose *loose, const struct object_id *oid, const struct object_id *compat_oid) { - struct odb_source_files *files = odb_source_files_downcast(source); - struct loose_object_map *map = files->loose->map; + struct loose_object_map *map = loose->map; int inserted = 0; inserted |= insert_oid_pair(map->to_compat, oid, compat_oid); inserted |= insert_oid_pair(map->to_storage, compat_oid, oid); if (inserted) - oidtree_insert(files->loose->cache, compat_oid, NULL); + oidtree_insert(loose->cache, compat_oid, NULL); return inserted; } -static int load_one_loose_object_map(struct repository *repo, struct odb_source *source) +static int load_one_loose_object_map(struct repository *repo, struct odb_source_loose *loose) { - struct odb_source_files *files = odb_source_files_downcast(source); struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT; FILE *fp; - if (!files->loose->map) - loose_object_map_init(&files->loose->map); - if (!files->loose->cache) { - ALLOC_ARRAY(files->loose->cache, 1); - oidtree_init(files->loose->cache); + if (!loose->map) + loose_object_map_init(&loose->map); + if (!loose->cache) { + ALLOC_ARRAY(loose->cache, 1); + oidtree_init(loose->cache); } - insert_loose_map(source, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree); - insert_loose_map(source, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob); - insert_loose_map(source, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid); + insert_loose_map(loose, repo->hash_algo->empty_tree, repo->compat_hash_algo->empty_tree); + insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob); + insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid); repo_common_path_replace(repo, &path, "objects/loose-object-idx"); fp = fopen(path.buf, "rb"); @@ -97,7 +95,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source parse_oid_hex_algop(p, &compat_oid, &p, repo->compat_hash_algo) || p != buf.buf + buf.len) goto err; - insert_loose_map(source, &oid, &compat_oid); + insert_loose_map(loose, &oid, &compat_oid); } strbuf_release(&buf); @@ -119,7 +117,8 @@ int repo_read_loose_object_map(struct repository *repo) odb_prepare_alternates(repo->objects); for (source = repo->objects->sources; source; source = source->next) { - if (load_one_loose_object_map(repo, source) < 0) { + struct odb_source_files *files = odb_source_files_downcast(source); + if (load_one_loose_object_map(repo, files->loose) < 0) { return -1; } } @@ -171,7 +170,7 @@ int repo_write_loose_object_map(struct repository *repo) return -1; } -static int write_one_object(struct odb_source *source, +static int write_one_object(struct odb_source_loose *loose, const struct object_id *oid, const struct object_id *compat_oid) { @@ -180,7 +179,7 @@ static int write_one_object(struct odb_source *source, struct stat st; struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT; - strbuf_addf(&path, "%s/loose-object-idx", source->path); + strbuf_addf(&path, "%s/loose-object-idx", loose->base.path); hold_lock_file_for_update_timeout(&lock, path.buf, LOCK_DIE_ON_ERROR, -1); fd = open(path.buf, O_WRONLY | O_CREAT | O_APPEND, 0666); @@ -196,7 +195,7 @@ static int write_one_object(struct odb_source *source, goto errout; if (close(fd)) goto errout; - adjust_shared_perm(source->odb->repo, path.buf); + adjust_shared_perm(loose->base.odb->repo, path.buf); rollback_lock_file(&lock); strbuf_release(&buf); strbuf_release(&path); @@ -210,18 +209,18 @@ static int write_one_object(struct odb_source *source, return -1; } -int repo_add_loose_object_map(struct odb_source *source, +int repo_add_loose_object_map(struct odb_source_loose *loose, const struct object_id *oid, const struct object_id *compat_oid) { int inserted = 0; - if (!should_use_loose_object_map(source->odb->repo)) + if (!should_use_loose_object_map(loose->base.odb->repo)) return 0; - inserted = insert_loose_map(source, oid, compat_oid); + inserted = insert_loose_map(loose, oid, compat_oid); if (inserted) - return write_one_object(source, oid, compat_oid); + return write_one_object(loose, oid, compat_oid); return 0; } diff --git a/loose.h b/loose.h index 6af1702973c058..6c9b3f4571602f 100644 --- a/loose.h +++ b/loose.h @@ -4,7 +4,7 @@ #include "khash.h" struct repository; -struct odb_source; +struct odb_source_loose; struct loose_object_map { kh_oid_map_t *to_compat; @@ -17,7 +17,7 @@ int repo_loose_object_map_oid(struct repository *repo, const struct object_id *src, const struct git_hash_algo *dest_algo, struct object_id *dest); -int repo_add_loose_object_map(struct odb_source *source, +int repo_add_loose_object_map(struct odb_source_loose *loose, const struct object_id *oid, const struct object_id *compat_oid); int repo_read_loose_object_map(struct repository *repo); diff --git a/object-file.c b/object-file.c index 0689a4e67b156a..fe24f00d1b79bf 100644 --- a/object-file.c +++ b/object-file.c @@ -810,6 +810,7 @@ int odb_source_loose_write_stream(struct odb_source *source, struct odb_write_stream *in_stream, size_t len, struct object_id *oid) { + struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; struct object_id compat_oid; int fd, ret, err = 0, flush = 0; @@ -918,7 +919,7 @@ int odb_source_loose_write_stream(struct odb_source *source, err = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf, FOF_SKIP_COLLISION_CHECK); if (!err && compat) - err = repo_add_loose_object_map(source, oid, &compat_oid); + err = repo_add_loose_object_map(files->loose, oid, &compat_oid); cleanup: strbuf_release(&tmp_file); strbuf_release(&filename); @@ -931,6 +932,7 @@ int odb_source_loose_write_object(struct odb_source *source, struct object_id *compat_oid_in, enum odb_write_object_flags flags) { + struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *algo = source->odb->repo->hash_algo; const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; struct object_id compat_oid; @@ -962,13 +964,14 @@ int odb_source_loose_write_object(struct odb_source *source, if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags)) return -1; if (compat) - return repo_add_loose_object_map(source, oid, &compat_oid); + return repo_add_loose_object_map(files->loose, oid, &compat_oid); return 0; } int force_object_loose(struct odb_source *source, const struct object_id *oid, time_t mtime) { + struct odb_source_files *files = odb_source_files_downcast(source); const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; void *buf; unsigned long len; @@ -998,7 +1001,7 @@ int force_object_loose(struct odb_source *source, hdrlen = format_object_header(hdr, sizeof(hdr), type, len); ret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0); if (!ret && compat) - ret = repo_add_loose_object_map(source, oid, &compat_oid); + ret = repo_add_loose_object_map(files->loose, oid, &compat_oid); free(buf); return ret; From 04a6e84cbdbebadd01d939168f1c69680c174fce Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:37 +0200 Subject: [PATCH 034/436] odb/source-loose: wire up `write_object()` callback Move `odb_source_loose_write_object()` from "object-file.c" into "odb/source-loose.c" and wire it up as the `write_object()` callback of the loose source. As in preceding commits, this requires us to expose a couple of generic functions from "object-file.c" as they are used in both subsystems now. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.c | 58 +++++++--------------------------------------- object-file.h | 14 ++++++----- odb/source-files.c | 5 ++-- odb/source-loose.c | 44 +++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 58 deletions(-) diff --git a/object-file.c b/object-file.c index fe24f00d1b79bf..7bb5b31bcad88b 100644 --- a/object-file.c +++ b/object-file.c @@ -326,10 +326,10 @@ static void hash_object_body(const struct git_hash_algo *algo, struct git_hash_c git_hash_final_oid(oid, c); } -static void write_object_file_prepare(const struct git_hash_algo *algo, - const void *buf, unsigned long len, - enum object_type type, struct object_id *oid, - char *hdr, int *hdrlen) +void write_object_file_prepare(const struct git_hash_algo *algo, + const void *buf, unsigned long len, + enum object_type type, struct object_id *oid, + char *hdr, int *hdrlen) { struct git_hash_ctx c; @@ -746,10 +746,10 @@ static int end_loose_object_common(struct odb_source *source, return Z_OK; } -static int write_loose_object(struct odb_source *source, - const struct object_id *oid, char *hdr, - int hdrlen, const void *buf, unsigned long len, - time_t mtime, unsigned flags) +int write_loose_object(struct odb_source *source, + const struct object_id *oid, char *hdr, + int hdrlen, const void *buf, unsigned long len, + time_t mtime, unsigned flags) { int fd, ret; unsigned char compressed[4096]; @@ -926,48 +926,6 @@ int odb_source_loose_write_stream(struct odb_source *source, return err; } -int odb_source_loose_write_object(struct odb_source *source, - const void *buf, unsigned long len, - enum object_type type, struct object_id *oid, - struct object_id *compat_oid_in, - enum odb_write_object_flags flags) -{ - struct odb_source_files *files = odb_source_files_downcast(source); - const struct git_hash_algo *algo = source->odb->repo->hash_algo; - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; - struct object_id compat_oid; - char hdr[MAX_HEADER_LEN]; - int hdrlen = sizeof(hdr); - - /* Generate compat_oid */ - if (compat) { - if (compat_oid_in) - oidcpy(&compat_oid, compat_oid_in); - else if (type == OBJ_BLOB) - hash_object_file(compat, buf, len, type, &compat_oid); - else { - struct strbuf converted = STRBUF_INIT; - convert_object_file(source->odb->repo, &converted, algo, compat, - buf, len, type, 0); - hash_object_file(compat, converted.buf, converted.len, - type, &compat_oid); - strbuf_release(&converted); - } - } - - /* Normally if we have it in the pack then we do not bother writing - * it out into .git/objects/??/?{38} file. - */ - write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); - if (odb_freshen_object(source->odb, oid)) - return 0; - if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags)) - return -1; - if (compat) - return repo_add_loose_object_map(files->loose, oid, &compat_oid); - return 0; -} - int force_object_loose(struct odb_source *source, const struct object_id *oid, time_t mtime) { diff --git a/object-file.h b/object-file.h index 1d90df9d98b78e..2b32592de1135b 100644 --- a/object-file.h +++ b/object-file.h @@ -23,12 +23,6 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -int odb_source_loose_write_object(struct odb_source *source, - const void *buf, unsigned long len, - enum object_type type, struct object_id *oid, - struct object_id *compat_oid_in, - enum odb_write_object_flags flags); - int odb_source_loose_write_stream(struct odb_source *source, struct odb_write_stream *stream, size_t len, struct object_id *oid); @@ -129,6 +123,14 @@ int finalize_object_file_flags(struct repository *repo, void hash_object_file(const struct git_hash_algo *algo, const void *buf, unsigned long len, enum object_type type, struct object_id *oid); +void write_object_file_prepare(const struct git_hash_algo *algo, + const void *buf, unsigned long len, + enum object_type type, struct object_id *oid, + char *hdr, int *hdrlen); +int write_loose_object(struct odb_source *source, + const struct object_id *oid, char *hdr, + int hdrlen, const void *buf, unsigned long len, + time_t mtime, unsigned flags); /* Helper to check and "touch" a file */ int check_and_freshen_file(const char *fn, int freshen); diff --git a/odb/source-files.c b/odb/source-files.c index ef548e6fe69cd0..52ba04237acfd7 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -164,8 +164,9 @@ static int odb_source_files_write_object(struct odb_source *source, struct object_id *compat_oid, enum odb_write_object_flags flags) { - return odb_source_loose_write_object(source, buf, len, type, - oid, compat_oid, flags); + struct odb_source_files *files = odb_source_files_downcast(source); + return odb_source_write_object(&files->loose->base, buf, len, type, + oid, compat_oid, flags); } static int odb_source_files_write_object_stream(struct odb_source *source, diff --git a/odb/source-loose.c b/odb/source-loose.c index e519365d23f680..c91018109e5b68 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -5,6 +5,7 @@ #include "hex.h" #include "loose.h" #include "object-file.h" +#include "object-file-convert.h" #include "odb.h" #include "odb/source-files.h" #include "odb/source-loose.h" @@ -588,6 +589,48 @@ static int odb_source_loose_freshen_object(struct odb_source *source, return !!check_and_freshen_file(path.buf, 1); } +static int odb_source_loose_write_object(struct odb_source *source, + const void *buf, unsigned long len, + enum object_type type, struct object_id *oid, + struct object_id *compat_oid_in, + enum odb_write_object_flags flags) +{ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + const struct git_hash_algo *algo = source->odb->repo->hash_algo; + const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + struct object_id compat_oid; + char hdr[MAX_HEADER_LEN]; + int hdrlen = sizeof(hdr); + + /* Generate compat_oid */ + if (compat) { + if (compat_oid_in) + oidcpy(&compat_oid, compat_oid_in); + else if (type == OBJ_BLOB) + hash_object_file(compat, buf, len, type, &compat_oid); + else { + struct strbuf converted = STRBUF_INIT; + convert_object_file(source->odb->repo, &converted, algo, compat, + buf, len, type, 0); + hash_object_file(compat, converted.buf, converted.len, + type, &compat_oid); + strbuf_release(&converted); + } + } + + /* Normally if we have it in the pack then we do not bother writing + * it out into .git/objects/??/?{38} file. + */ + write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); + if (odb_freshen_object(source->odb, oid)) + return 0; + if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags)) + return -1; + if (compat) + return repo_add_loose_object_map(loose, oid, &compat_oid); + return 0; +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -647,6 +690,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.find_abbrev_len = odb_source_loose_find_abbrev_len; loose->base.count_objects = odb_source_loose_count_objects; loose->base.freshen_object = odb_source_loose_freshen_object; + loose->base.write_object = odb_source_loose_write_object; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From b9906a645c38ef77643d661ac9a5a6aa31fbeaf4 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:38 +0200 Subject: [PATCH 035/436] object-file: refactor writing objects to use loose source The "object-file" subsystem still hosts the majority of logic used to write loose objects. Eventually, we'll want to move this logic into "odb/source-loose.c", but this isn't yet easily possible because a lot of the writing logic is still being shared with `force_object_loose()`. We will eventually detangle this logic so that we can indeed move all of it into the "loose" source. Meanwhile though, refactor the code so that it operates on a `struct odb_source_loose` directly to already make the dependency explicit. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- http-walker.c | 3 +- http.c | 6 ++-- object-file.c | 75 +++++++++++++++++++++++----------------------- object-file.h | 6 ++-- odb/source-files.c | 3 +- odb/source-loose.c | 9 +++--- 6 files changed, 53 insertions(+), 49 deletions(-) diff --git a/http-walker.c b/http-walker.c index 1b6d496548373e..435a7265408fa4 100644 --- a/http-walker.c +++ b/http-walker.c @@ -539,8 +539,9 @@ static int fetch_object(struct walker *walker, const struct object_id *oid) } else if (!oideq(&obj_req->oid, &req->real_oid)) { ret = error("File %s has bad hash", hex); } else if (req->rename < 0) { + struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); struct strbuf buf = STRBUF_INIT; - odb_loose_path(the_repository->objects->sources, &buf, &req->oid); + odb_loose_path(files->loose, &buf, &req->oid); ret = error("unable to write sha1 filename %s", buf.buf); strbuf_release(&buf); } diff --git a/http.c b/http.c index ea9b16861bc3d4..3fcc0122337ba4 100644 --- a/http.c +++ b/http.c @@ -2826,6 +2826,7 @@ static size_t fwrite_sha1_file(char *ptr, size_t eltsize, size_t nmemb, struct http_object_request *new_http_object_request(const char *base_url, const struct object_id *oid) { + struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); char *hex = oid_to_hex(oid); struct strbuf filename = STRBUF_INIT; struct strbuf prevfile = STRBUF_INIT; @@ -2840,7 +2841,7 @@ struct http_object_request *new_http_object_request(const char *base_url, oidcpy(&freq->oid, oid); freq->localfile = -1; - odb_loose_path(the_repository->objects->sources, &filename, oid); + odb_loose_path(files->loose, &filename, oid); strbuf_addf(&freq->tmpfile, "%s.temp", filename.buf); strbuf_addf(&prevfile, "%s.prev", filename.buf); @@ -2966,6 +2967,7 @@ void process_http_object_request(struct http_object_request *freq) int finish_http_object_request(struct http_object_request *freq) { + struct odb_source_files *files = odb_source_files_downcast(the_repository->objects->sources); struct stat st; struct strbuf filename = STRBUF_INIT; @@ -2992,7 +2994,7 @@ int finish_http_object_request(struct http_object_request *freq) unlink_or_warn(freq->tmpfile.buf); return -1; } - odb_loose_path(the_repository->objects->sources, &filename, &freq->oid); + odb_loose_path(files->loose, &filename, &freq->oid); freq->rename = finalize_object_file(the_repository, freq->tmpfile.buf, filename.buf); strbuf_release(&filename); diff --git a/object-file.c b/object-file.c index 7bb5b31bcad88b..bce941874eb994 100644 --- a/object-file.c +++ b/object-file.c @@ -54,14 +54,14 @@ static void fill_loose_path(struct strbuf *buf, } } -const char *odb_loose_path(struct odb_source *source, +const char *odb_loose_path(struct odb_source_loose *loose, struct strbuf *buf, const struct object_id *oid) { strbuf_reset(buf); - strbuf_addstr(buf, source->path); + strbuf_addstr(buf, loose->base.path); strbuf_addch(buf, '/'); - fill_loose_path(buf, oid, source->odb->repo->hash_algo); + fill_loose_path(buf, oid, loose->base.odb->repo->hash_algo); return buf->buf; } @@ -575,14 +575,14 @@ static void flush_loose_object_transaction(struct odb_transaction_files *transac } /* Finalize a file on disk, and close it. */ -static void close_loose_object(struct odb_source *source, +static void close_loose_object(struct odb_source_loose *loose, int fd, const char *filename) { - if (source->will_destroy) + if (loose->base.will_destroy) goto out; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - fsync_loose_object_transaction(source->odb->transaction, fd, filename); + fsync_loose_object_transaction(loose->base.odb->transaction, fd, filename); else if (fsync_object_files > 0) fsync_or_die(fd, filename); else @@ -651,7 +651,7 @@ static int create_tmpfile(struct repository *repo, * Returns a "fd", which should later be provided to * end_loose_object_common(). */ -static int start_loose_object_common(struct odb_source *source, +static int start_loose_object_common(struct odb_source_loose *loose, struct strbuf *tmp_file, const char *filename, unsigned flags, git_zstream *stream, @@ -659,18 +659,18 @@ static int start_loose_object_common(struct odb_source *source, struct git_hash_ctx *c, struct git_hash_ctx *compat_c, char *hdr, int hdrlen) { - const struct git_hash_algo *algo = source->odb->repo->hash_algo; - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + const struct git_hash_algo *algo = loose->base.odb->repo->hash_algo; + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; int fd; - fd = create_tmpfile(source->odb->repo, tmp_file, filename); + fd = create_tmpfile(loose->base.odb->repo, tmp_file, filename); if (fd < 0) { if (flags & ODB_WRITE_OBJECT_SILENT) return -1; else if (errno == EACCES) return error(_("insufficient permission for adding " "an object to repository database %s"), - source->path); + loose->base.path); else return error_errno( _("unable to create temporary file")); @@ -700,14 +700,14 @@ static int start_loose_object_common(struct odb_source *source, * Common steps for the inner git_deflate() loop for writing loose * objects. Returns what git_deflate() returns. */ -static int write_loose_object_common(struct odb_source *source, +static int write_loose_object_common(struct odb_source_loose *loose, struct git_hash_ctx *c, struct git_hash_ctx *compat_c, git_zstream *stream, const int flush, unsigned char *in0, const int fd, unsigned char *compressed, const size_t compressed_len) { - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; int ret; ret = git_deflate(stream, flush ? Z_FINISH : 0); @@ -728,12 +728,12 @@ static int write_loose_object_common(struct odb_source *source, * - End the compression of zlib stream. * - Get the calculated oid to "oid". */ -static int end_loose_object_common(struct odb_source *source, +static int end_loose_object_common(struct odb_source_loose *loose, struct git_hash_ctx *c, struct git_hash_ctx *compat_c, git_zstream *stream, struct object_id *oid, struct object_id *compat_oid) { - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; int ret; ret = git_deflate_end_gently(stream); @@ -746,7 +746,7 @@ static int end_loose_object_common(struct odb_source *source, return Z_OK; } -int write_loose_object(struct odb_source *source, +int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, time_t mtime, unsigned flags) @@ -760,11 +760,11 @@ int write_loose_object(struct odb_source *source, static struct strbuf filename = STRBUF_INIT; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - prepare_loose_object_transaction(source->odb->transaction); + prepare_loose_object_transaction(loose->base.odb->transaction); - odb_loose_path(source, &filename, oid); + odb_loose_path(loose, &filename, oid); - fd = start_loose_object_common(source, &tmp_file, filename.buf, flags, + fd = start_loose_object_common(loose, &tmp_file, filename.buf, flags, &stream, compressed, sizeof(compressed), &c, NULL, hdr, hdrlen); if (fd < 0) @@ -776,14 +776,14 @@ int write_loose_object(struct odb_source *source, do { unsigned char *in0 = stream.next_in; - ret = write_loose_object_common(source, &c, NULL, &stream, 1, in0, fd, + ret = write_loose_object_common(loose, &c, NULL, &stream, 1, in0, fd, compressed, sizeof(compressed)); } while (ret == Z_OK); if (ret != Z_STREAM_END) die(_("unable to deflate new object %s (%d)"), oid_to_hex(oid), ret); - ret = end_loose_object_common(source, &c, NULL, &stream, ¶no_oid, NULL); + ret = end_loose_object_common(loose, &c, NULL, &stream, ¶no_oid, NULL); if (ret != Z_OK) die(_("deflateEnd on object %s failed (%d)"), oid_to_hex(oid), ret); @@ -791,7 +791,7 @@ int write_loose_object(struct odb_source *source, die(_("confused by unstable object source data for %s"), oid_to_hex(oid)); - close_loose_object(source, fd, tmp_file.buf); + close_loose_object(loose, fd, tmp_file.buf); if (mtime) { struct utimbuf utb; @@ -802,16 +802,15 @@ int write_loose_object(struct odb_source *source, warning_errno(_("failed utime() on %s"), tmp_file.buf); } - return finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf, + return finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, FOF_SKIP_COLLISION_CHECK); } -int odb_source_loose_write_stream(struct odb_source *source, +int odb_source_loose_write_stream(struct odb_source_loose *loose, struct odb_write_stream *in_stream, size_t len, struct object_id *oid) { - struct odb_source_files *files = odb_source_files_downcast(source); - const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; + const struct git_hash_algo *compat = loose->base.odb->repo->compat_hash_algo; struct object_id compat_oid; int fd, ret, err = 0, flush = 0; unsigned char compressed[4096]; @@ -825,10 +824,10 @@ int odb_source_loose_write_stream(struct odb_source *source, int hdrlen; if (batch_fsync_enabled(FSYNC_COMPONENT_LOOSE_OBJECT)) - prepare_loose_object_transaction(source->odb->transaction); + prepare_loose_object_transaction(loose->base.odb->transaction); /* Since oid is not determined, save tmp file to odb path. */ - strbuf_addf(&filename, "%s/", source->path); + strbuf_addf(&filename, "%s/", loose->base.path); hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len); /* @@ -839,7 +838,7 @@ int odb_source_loose_write_stream(struct odb_source *source, * - Setup zlib stream for compression. * - Start to feed header to zlib stream. */ - fd = start_loose_object_common(source, &tmp_file, filename.buf, 0, + fd = start_loose_object_common(loose, &tmp_file, filename.buf, 0, &stream, compressed, sizeof(compressed), &c, &compat_c, hdr, hdrlen); if (fd < 0) { @@ -867,7 +866,7 @@ int odb_source_loose_write_stream(struct odb_source *source, if (in_stream->is_finished) flush = 1; } - ret = write_loose_object_common(source, &c, &compat_c, &stream, flush, in0, fd, + ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd, compressed, sizeof(compressed)); /* * Unlike write_loose_object(), we do not have the entire @@ -890,16 +889,16 @@ int odb_source_loose_write_stream(struct odb_source *source, */ if (ret != Z_STREAM_END) die(_("unable to stream deflate new object (%d)"), ret); - ret = end_loose_object_common(source, &c, &compat_c, &stream, oid, &compat_oid); + ret = end_loose_object_common(loose, &c, &compat_c, &stream, oid, &compat_oid); if (ret != Z_OK) die(_("deflateEnd on stream object failed (%d)"), ret); - close_loose_object(source, fd, tmp_file.buf); + close_loose_object(loose, fd, tmp_file.buf); - if (odb_freshen_object(source->odb, oid)) { + if (odb_freshen_object(loose->base.odb, oid)) { unlink_or_warn(tmp_file.buf); goto cleanup; } - odb_loose_path(source, &filename, oid); + odb_loose_path(loose, &filename, oid); /* We finally know the object path, and create the missing dir. */ dirlen = directory_size(filename.buf); @@ -907,7 +906,7 @@ int odb_source_loose_write_stream(struct odb_source *source, struct strbuf dir = STRBUF_INIT; strbuf_add(&dir, filename.buf, dirlen); - if (safe_create_dir_in_gitdir(source->odb->repo, dir.buf) && + if (safe_create_dir_in_gitdir(loose->base.odb->repo, dir.buf) && errno != EEXIST) { err = error_errno(_("unable to create directory %s"), dir.buf); strbuf_release(&dir); @@ -916,10 +915,10 @@ int odb_source_loose_write_stream(struct odb_source *source, strbuf_release(&dir); } - err = finalize_object_file_flags(source->odb->repo, tmp_file.buf, filename.buf, + err = finalize_object_file_flags(loose->base.odb->repo, tmp_file.buf, filename.buf, FOF_SKIP_COLLISION_CHECK); if (!err && compat) - err = repo_add_loose_object_map(files->loose, oid, &compat_oid); + err = repo_add_loose_object_map(loose, oid, &compat_oid); cleanup: strbuf_release(&tmp_file); strbuf_release(&filename); @@ -957,7 +956,7 @@ int force_object_loose(struct odb_source *source, oid_to_hex(oid), compat->name); } hdrlen = format_object_header(hdr, sizeof(hdr), type, len); - ret = write_loose_object(source, oid, hdr, hdrlen, buf, len, mtime, 0); + ret = write_loose_object(files->loose, oid, hdr, hdrlen, buf, len, mtime, 0); if (!ret && compat) ret = repo_add_loose_object_map(files->loose, oid, &compat_oid); free(buf); diff --git a/object-file.h b/object-file.h index 2b32592de1135b..d30f1b10b2eb36 100644 --- a/object-file.h +++ b/object-file.h @@ -23,7 +23,7 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -int odb_source_loose_write_stream(struct odb_source *source, +int odb_source_loose_write_stream(struct odb_source_loose *loose, struct odb_write_stream *stream, size_t len, struct object_id *oid); @@ -31,7 +31,7 @@ int odb_source_loose_write_stream(struct odb_source *source, * Put in `buf` the name of the file in the local object database that * would be used to store a loose object with the specified oid. */ -const char *odb_loose_path(struct odb_source *source, +const char *odb_loose_path(struct odb_source_loose *source, struct strbuf *buf, const struct object_id *oid); @@ -127,7 +127,7 @@ void write_object_file_prepare(const struct git_hash_algo *algo, const void *buf, unsigned long len, enum object_type type, struct object_id *oid, char *hdr, int *hdrlen); -int write_loose_object(struct odb_source *source, +int write_loose_object(struct odb_source_loose *loose, const struct object_id *oid, char *hdr, int hdrlen, const void *buf, unsigned long len, time_t mtime, unsigned flags); diff --git a/odb/source-files.c b/odb/source-files.c index 52ba04237acfd7..2ba1def776e006 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -174,7 +174,8 @@ static int odb_source_files_write_object_stream(struct odb_source *source, size_t len, struct object_id *oid) { - return odb_source_loose_write_stream(source, stream, len, oid); + struct odb_source_files *files = odb_source_files_downcast(source); + return odb_source_loose_write_stream(files->loose, stream, len, oid); } static int odb_source_files_begin_transaction(struct odb_source *source, diff --git a/odb/source-loose.c b/odb/source-loose.c index c91018109e5b68..da8a60dba1c04c 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -220,7 +220,7 @@ static int odb_source_loose_read_object_info(struct odb_source *source, if (flags & OBJECT_INFO_SECOND_READ) return -1; - odb_loose_path(source, &buf, oid); + odb_loose_path(loose, &buf, oid); return read_object_info_from_path(loose, buf.buf, oid, oi, flags); } @@ -238,7 +238,7 @@ static int open_loose_object(struct odb_source_loose *loose, static struct strbuf buf = STRBUF_INIT; int fd; - *path = odb_loose_path(&loose->base, &buf, oid); + *path = odb_loose_path(loose, &buf, oid); fd = git_open(*path); if (fd >= 0) return fd; @@ -584,8 +584,9 @@ static int odb_source_loose_count_objects(struct odb_source *source, static int odb_source_loose_freshen_object(struct odb_source *source, const struct object_id *oid) { + struct odb_source_loose *loose = odb_source_loose_downcast(source); static struct strbuf path = STRBUF_INIT; - odb_loose_path(source, &path, oid); + odb_loose_path(loose, &path, oid); return !!check_and_freshen_file(path.buf, 1); } @@ -624,7 +625,7 @@ static int odb_source_loose_write_object(struct odb_source *source, write_object_file_prepare(algo, buf, len, type, oid, hdr, &hdrlen); if (odb_freshen_object(source->odb, oid)) return 0; - if (write_loose_object(source, oid, hdr, hdrlen, buf, len, 0, flags)) + if (write_loose_object(loose, oid, hdr, hdrlen, buf, len, 0, flags)) return -1; if (compat) return repo_add_loose_object_map(loose, oid, &compat_oid); From e6a39bbe7a6bde5fb7de8d487e8f4ef928e6b751 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:39 +0200 Subject: [PATCH 036/436] odb/source-loose: wire up `write_object_stream()` callback Wire up the `write_object_stream()` callback. Note that we don't move the implementation into "odb/source-loose.c". This is because most of the logic to write loose objects is still contained in "object-file.c", and detangling that requires us to do some refactorings as explained in the preceding commit. So for now, the implementation of writing an object stream is still located in "object-file.c". Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- object-file.h | 12 +++++++++++- odb/source-files.c | 3 ++- odb/source-loose.c | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/object-file.h b/object-file.h index d30f1b10b2eb36..528c4e6e697f87 100644 --- a/object-file.h +++ b/object-file.h @@ -23,7 +23,17 @@ int index_path(struct index_state *istate, struct object_id *oid, const char *pa struct object_info; struct odb_source; -int odb_source_loose_write_stream(struct odb_source_loose *loose, +/* + * Write the given stream into the loose object source. The only difference + * from the generic implementation of this function is that we don't perform an + * object existence check here. + * + * TODO: We should stop exposing this function altogether and move it into + * "odb/source-loose.c". This requires a couple of refactorings though to make + * `force_object_loose()` generic and is thus postponed to a later point in + * time. + */ +int odb_source_loose_write_stream(struct odb_source_loose *source, struct odb_write_stream *stream, size_t len, struct object_id *oid); diff --git a/odb/source-files.c b/odb/source-files.c index 2ba1def776e006..83f8066c67dd3c 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -7,6 +7,7 @@ #include "odb.h" #include "odb/source.h" #include "odb/source-files.h" +#include "odb/source-loose.h" #include "packfile.h" #include "strbuf.h" #include "write-or-die.h" @@ -175,7 +176,7 @@ static int odb_source_files_write_object_stream(struct odb_source *source, struct object_id *oid) { struct odb_source_files *files = odb_source_files_downcast(source); - return odb_source_loose_write_stream(files->loose, stream, len, oid); + return odb_source_write_object_stream(&files->loose->base, stream, len, oid); } static int odb_source_files_begin_transaction(struct odb_source *source, diff --git a/odb/source-loose.c b/odb/source-loose.c index da8a60dba1c04c..e52fc289a24102 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -632,6 +632,19 @@ static int odb_source_loose_write_object(struct odb_source *source, return 0; } +static int odb_source_loose_write_object_stream(struct odb_source *source, + struct odb_write_stream *in_stream, + size_t len, + struct object_id *oid) +{ + /* + * TODO: the implementation should be moved here, see the comment on + * the called function in "object-file.h". + */ + struct odb_source_loose *loose = odb_source_loose_downcast(source); + return odb_source_loose_write_stream(loose, in_stream, len, oid); +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -692,6 +705,7 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.count_objects = odb_source_loose_count_objects; loose->base.freshen_object = odb_source_loose_freshen_object; loose->base.write_object = odb_source_loose_write_object; + loose->base.write_object_stream = odb_source_loose_write_object_stream; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From 87af3bb434b86805f69fae40c966d92db1bd2eae Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:40 +0200 Subject: [PATCH 037/436] odb/source-loose: stub out remaining callbacks Stub out remaining callback functions for the "loose" backend. Note that we also stub out transactions for loose objects. In fact, we already have the infrastructure in place for those, and we could in theory implement those, as well. But there are separate efforts ongoing to polish up transactional interfaces, and doing so now would likely result in some messiness. This omission will thus be worked on in a subsequent patch series, once the dust has settled. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-loose.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/odb/source-loose.c b/odb/source-loose.c index e52fc289a24102..e1749413184160 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -645,6 +645,25 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, return odb_source_loose_write_stream(loose, in_stream, len, oid); } +static int odb_source_loose_begin_transaction(struct odb_source *source UNUSED, + struct odb_transaction **out UNUSED) +{ + /* TODO: this is a known omission that we'll want to address eventually. */ + return error("loose source does not support transactions"); +} + +static int odb_source_loose_read_alternates(struct odb_source *source UNUSED, + struct strvec *out UNUSED) +{ + return 0; +} + +static int odb_source_loose_write_alternate(struct odb_source *source UNUSED, + const char *alternate UNUSED) +{ + return error("loose source does not support alternates"); +} + static void odb_source_loose_clear_cache(struct odb_source_loose *loose) { oidtree_clear(loose->cache); @@ -706,6 +725,9 @@ struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) loose->base.freshen_object = odb_source_loose_freshen_object; loose->base.write_object = odb_source_loose_write_object; loose->base.write_object_stream = odb_source_loose_write_object_stream; + loose->base.begin_transaction = odb_source_loose_begin_transaction; + loose->base.read_alternates = odb_source_loose_read_alternates; + loose->base.write_alternate = odb_source_loose_write_alternate; if (!is_absolute_path(loose->base.path)) chdir_notify_register(NULL, odb_source_loose_reparent, loose); From ef4778bcba323ab38d442811f851af092760b6b5 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Mon, 1 Jun 2026 10:20:41 +0200 Subject: [PATCH 038/436] odb/source-loose: drop pointer to the "files" source Now that all callbacks of the loose source operate on `struct odb_source_loose` directly we no longer have to reach into the "files" source at all. Drop this field and update `odb_source_loose_new()` to instead accept all parameters required to initialize itself. This ensures that the "loose" backend is a fully standalone source. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- odb/source-files.c | 2 +- odb/source-loose.c | 8 ++++---- odb/source-loose.h | 7 ++++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/odb/source-files.c b/odb/source-files.c index 83f8066c67dd3c..5bdd0429225397 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -268,7 +268,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, CALLOC_ARRAY(files, 1); odb_source_init(&files->base, odb, ODB_SOURCE_FILES, path, local); - files->loose = odb_source_loose_new(files); + files->loose = odb_source_loose_new(odb, path, local); files->packed = packfile_store_new(&files->base); files->base.free = odb_source_files_free; diff --git a/odb/source-loose.c b/odb/source-loose.c index e1749413184160..7d7ea2fb842537 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -705,14 +705,14 @@ static void odb_source_loose_free(struct odb_source *source) free(loose); } -struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files) +struct odb_source_loose *odb_source_loose_new(struct object_database *odb, + const char *path, + bool local) { struct odb_source_loose *loose; CALLOC_ARRAY(loose, 1); - odb_source_init(&loose->base, files->base.odb, ODB_SOURCE_LOOSE, - files->base.path, files->base.local); - loose->files = files; + odb_source_init(&loose->base, odb, ODB_SOURCE_LOOSE, path, local); loose->base.free = odb_source_loose_free; loose->base.close = odb_source_loose_close; diff --git a/odb/source-loose.h b/odb/source-loose.h index 4dd4fd6ce30a7e..6070aaf3ce6ab2 100644 --- a/odb/source-loose.h +++ b/odb/source-loose.h @@ -9,11 +9,10 @@ struct oidtree; /* * An object database source that stores its objects in loose format, one - * file per object. This source is part of the files source. + * file per object. */ struct odb_source_loose { struct odb_source base; - struct odb_source_files *files; /* * Used to store the results of readdir(3) calls when we are OK @@ -31,7 +30,9 @@ struct odb_source_loose { struct loose_object_map *map; }; -struct odb_source_loose *odb_source_loose_new(struct odb_source_files *files); +struct odb_source_loose *odb_source_loose_new(struct object_database *odb, + const char *path, + bool local); /* * Cast the given object database source to the loose backend. This will cause From 061a68e4433b98ca351dcbf887b890aa2ec1bdb8 Mon Sep 17 00:00:00 2001 From: Michael Montalbo Date: Mon, 1 Jun 2026 21:20:47 +0000 Subject: [PATCH 039/436] sub-process: use gentle handshake to avoid die() on startup failure When the configured subprocess command contains shell metacharacters (such as a space), prepare_shell_cmd() wraps it in "sh -c ". The shell itself always starts successfully, so start_command() returns zero even if the tool inside does not exist. The subsequent handshake then reads from a dead pipe and calls die() via the non-gentle packet_read_line(), killing the parent process instead of letting it handle the error. Before this change, a missing filter process at a path containing spaces produces a confusing error: $ git -c filter.myfilter.process="/path with space/tool" \ -c filter.myfilter.required=true add file.txt /path with space/tool: line 1: /path: No such file or directory fatal: the remote end hung up unexpectedly After this change, the proper error is reported: $ git ... add file.txt /path with space/tool: line 1: /path: No such file or directory error: could not read greeting from subprocess '/path with space/tool' error: initialization for subprocess '/path with space/tool' failed fatal: file.txt: clean filter 'myfilter' failed Switch the subprocess handshake from the dying packet_read_line() to packet_read_line_gently() so that a process that exits during startup produces an error return instead of killing the caller. This affects any subprocess consumer whose command path contains spaces. On Windows this routinely happens because programs live under "C:/Program Files/...", and MSYS2 path conversion can rewrite absolute paths to include that prefix. On POSIX it triggers whenever the configured path naturally contains a space or other metacharacter. convert.c (filter..process, used by git-lfs and custom clean/smudge filters) is the primary affected consumer. Signed-off-by: Michael Montalbo Signed-off-by: Junio C Hamano --- sub-process.c | 26 ++++++++++++++++++++------ t/t0021-conversion.sh | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/sub-process.c b/sub-process.c index 83bf0a0e82e56d..2d5c965169727b 100644 --- a/sub-process.c +++ b/sub-process.c @@ -132,17 +132,24 @@ static int handshake_version(struct child_process *process, if (packet_flush_gently(process->in)) return error("Could not write flush packet"); - if (!(line = packet_read_line(process->out, NULL)) || - !skip_prefix(line, welcome_prefix, &p) || + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read greeting from subprocess '%s'", + process->args.v[0]); + if (!line || !skip_prefix(line, welcome_prefix, &p) || strcmp(p, "-server")) return error("Unexpected line '%s', expected %s-server", line ? line : "", welcome_prefix); - if (!(line = packet_read_line(process->out, NULL)) || - !skip_prefix(line, "version=", &p) || + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read version from subprocess '%s'", + process->args.v[0]); + if (!line || !skip_prefix(line, "version=", &p) || strtol_i(p, 10, chosen_version)) return error("Unexpected line '%s', expected version", line ? line : ""); - if ((line = packet_read_line(process->out, NULL))) + if (packet_read_line_gently(process->out, NULL, &line) < 0) + return error("could not read version flush from subprocess '%s'", + process->args.v[0]); + if (line) return error("Unexpected line '%s', expected flush", line); /* Check to make sure that the version received is supported */ @@ -171,8 +178,15 @@ static int handshake_capabilities(struct child_process *process, if (packet_flush_gently(process->in)) return error("Could not write flush packet"); - while ((line = packet_read_line(process->out, NULL))) { + for (;;) { const char *p; + int len = packet_read_line_gently(process->out, NULL, &line); + + if (len < 0) + return error("could not read capabilities from subprocess '%s'", + process->args.v[0]); + if (!line) + break; if (!skip_prefix(line, "capability=", &p)) continue; diff --git a/t/t0021-conversion.sh b/t/t0021-conversion.sh index f0d50d769e9fc5..033b00a364ee7a 100755 --- a/t/t0021-conversion.sh +++ b/t/t0021-conversion.sh @@ -857,6 +857,23 @@ test_expect_success 'invalid process filter must fail (and not hang!)' ' ) ' +test_expect_success 'missing process filter with space in path does not die' ' + test_config_global filter.protocol.process "/non existent/tool" && + test_config_global filter.protocol.required true && + rm -rf repo && + mkdir repo && + ( + cd repo && + git init && + + echo "*.r filter=protocol" >.gitattributes && + + cp "$TEST_ROOT/test.o" test.r && + test_must_fail git add . 2>git-stderr.log && + test_grep "clean filter.*protocol.*failed" git-stderr.log + ) +' + test_expect_success 'delayed checkout in process filter' ' test_config_global filter.a.process "test-tool rot13-filter --log=a.log clean smudge delay" && test_config_global filter.a.required true && From 96ee7f1650e6096561599f069d18c052412d7506 Mon Sep 17 00:00:00 2001 From: LorenzoPegorari Date: Mon, 1 Jun 2026 15:52:01 +0200 Subject: [PATCH 040/436] http: cleanup function fetch_and_setup_pack_index() Cleanup the function `fetch_and_setup_pack_index()` by removing the useless call to the function `unlink()`. This is not necessary anymore since 63aca3f7f1 (dumb-http: store downloaded pack idx as tempfile, 2024-10-25), when `fetch_pack_index()` started registering its return value (in this case `tmp_idx`) as a tempfile to be deleted at process exit. Signed-off-by: LorenzoPegorari Signed-off-by: Junio C Hamano --- http.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/http.c b/http.c index ea9b16861bc3d4..55dd856a279a23 100644 --- a/http.c +++ b/http.c @@ -2609,9 +2609,7 @@ static int fetch_and_setup_pack_index(struct packfile_list *packs, new_pack = parse_pack_index(the_repository, sha1, tmp_idx); if (!new_pack) { - unlink(tmp_idx); free(tmp_idx); - return -1; /* parse_pack_index() already issued error message */ } From 18decad922884a69ea39c0332f7a94ce82cf99cc Mon Sep 17 00:00:00 2001 From: LorenzoPegorari Date: Mon, 1 Jun 2026 15:52:12 +0200 Subject: [PATCH 041/436] http: fix memory leak in fetch_and_setup_pack_index() Inside the function `fetch_and_setup_pack_index()`, when the pack obtained using `parse_pack_index()` fails to be verified by `verify_pack_index()`, the function returns without closing and freeing said pack. Fix this by calling `close_pack_index()` to munmap the index file for the leaking pack (which might have been mmapped by `fetch_pack_index()` or `verify_pack_index()`), and then free it, when the verification fails. Signed-off-by: LorenzoPegorari Signed-off-by: Junio C Hamano --- http.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/http.c b/http.c index 55dd856a279a23..d50a34e4460a90 100644 --- a/http.c +++ b/http.c @@ -2614,11 +2614,13 @@ static int fetch_and_setup_pack_index(struct packfile_list *packs, } ret = verify_pack_index(new_pack); - if (!ret) - close_pack_index(new_pack); + + close_pack_index(new_pack); free(tmp_idx); - if (ret) + if (ret) { + free(new_pack); return -1; + } packfile_list_prepend(packs, new_pack); return 0; From 6e670b3f0c3b80a9d417d5fef36904a21161587a Mon Sep 17 00:00:00 2001 From: Arijit Banerjee Date: Mon, 1 Jun 2026 16:13:21 +0000 Subject: [PATCH 042/436] index-pack: retain child bases in delta cache When resolving a delta whose result has children of its own, index-pack adds the result to work_head, accounts its data in base_cache_used, and calls prune_base_data(). It then immediately frees that same data. This bypasses the existing delta base cache policy and can force later descendants to reconstruct the queued base again. Let the existing delta_base_cache_limit pruning policy decide whether to keep or evict the data instead. This does not add a new cache or increase the cache limit. The object data is already accounted in base_cache_used before prune_base_data() runs, and the existing pruning and base cleanup paths still release it. On a quiet Ubuntu 24.04 VM with 16 vCPUs, 32 GiB RAM, and local SSD, direct index-pack timings on single-pack Linux fixtures improved as follows: linux blobless: 69.17s -> 57.98s (16.2% faster), RSS flat linux full: 280.72s -> 236.32s (15.8% faster), RSS +1.9% Five-repeat medians on public repositories also improved: git.git: 12.31s -> 10.70s (13.1% faster) libgit2: 3.35s -> 2.88s (14.0% faster) redis: 6.52s -> 5.64s (13.5% faster) cpython: 33.02s -> 31.44s (4.8% faster) The standard p5302 perf test on a smaller git.git fixture was neutral: 5302.9 index-pack default threads: 11.21(38.07+1.33) -> 11.16(37.90+1.31), -0.4% t/t5302-pack-index.sh passed, and GitGitGadget's linux-leaks CI also exercised that test under SANITIZE=leak. Signed-off-by: Arijit Banerjee Signed-off-by: Junio C Hamano --- builtin/index-pack.c | 1 - 1 file changed, 1 deletion(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index cf0bd8280dca83..027c64b522ebb2 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -1212,7 +1212,6 @@ static void *threaded_second_pass(void *data) list_add(&child->list, &work_head); base_cache_used += child->size; prune_base_data(NULL); - free_base_data(child); } else if (child) { /* * This child does not have its own children. It may be From 1891707d1b8bb0ac3c47343e881fcf28ec69457a Mon Sep 17 00:00:00 2001 From: Jacob Keller Date: Mon, 1 Jun 2026 16:36:08 -0700 Subject: [PATCH 043/436] describe: fix --exclude, --match with --contains and --all git describe --contains acts as a wrapper around git name-rev. When operating with --contains and --all, the --match and --exclude patterns are not properly forwarded to name-rev as --exclude and --refs options. This results in the command silently discarding match and exclude requests from the user when operating in --all mode. We could check and die() if the user provides --contains, --all, and --match/--exclude. However, its also straight forward to just pass the filters down to git name-rev. Notice that the documentation for --match and --exclude mention the --all mode. It explains that they operate on refs with the prefix refs/tags, and additionally refs/heads and refs/remotes when using --all. Fix the describe logic to pass the patterns down with the appropriate prefixes when --all is provided. This fixes the support to match the documented behavior. Add tests to check that this works as expected. Reported-by: Tuomas Ahola Signed-off-by: Jacob Keller Signed-off-by: Junio C Hamano --- builtin/describe.c | 18 +++++++++++++++--- t/t6120-describe.sh | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/builtin/describe.c b/builtin/describe.c index bffeed13a3cb14..62800ef15ed915 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -712,13 +712,25 @@ int cmd_describe(int argc, NULL); if (always) strvec_push(&args, "--always"); - if (!all) { + if (!all) strvec_push(&args, "--tags"); + + for_each_string_list_item(item, &patterns) + strvec_pushf(&args, "--refs=refs/tags/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + strvec_pushf(&args, "--exclude=refs/tags/%s", item->string); + + if (all) { for_each_string_list_item(item, &patterns) - strvec_pushf(&args, "--refs=refs/tags/%s", item->string); + strvec_pushf(&args, "--refs=refs/heads/%s", item->string); for_each_string_list_item(item, &exclude_patterns) - strvec_pushf(&args, "--exclude=refs/tags/%s", item->string); + strvec_pushf(&args, "--exclude=refs/heads/%s", item->string); + for_each_string_list_item(item, &patterns) + strvec_pushf(&args, "--refs=refs/remotes/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + strvec_pushf(&args, "--exclude=refs/remotes/%s", item->string); } + if (argc) strvec_pushv(&args, argv); else diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 2c70cc561ad5f6..e5bcf537602a21 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -345,6 +345,28 @@ test_expect_success 'describe --contains and --no-match' ' test_cmp expect actual ' +test_expect_success 'describe --contains --all --match no matching commit' ' + echo "tags/A^0" >expect && + tagged_commit=$(git rev-parse "refs/tags/A^0") && + test_must_fail git describe --contains --all --match="B" $tagged_commit +' + +check_describe "tags/A^0" --contains --all --match="A" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_A" --contains --all --match="branch*" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_C~1" --contains --all --match="branch*" --exclude="branch_A" $(git rev-parse "refs/tags/A^0") + +check_describe "branch_A" --contains --all \ + --exclude="A" --exclude="c" --exclude="test*" --exclude="origin/remote_branch_A" \ + $(git rev-parse "refs/tags/A^0") + +check_describe "remotes/origin/remote_branch_A" --contains --all --match="origin/remote*" $(git rev-parse "refs/tags/A^0") + +check_describe "remotes/origin/remote_branch_C~1" --contains --all \ + --match="origin/remote*" --exclude="origin/remote_branch_A" \ + $(git rev-parse "refs/tags/A^0") + test_expect_success 'setup and absorb a submodule' ' test_create_repo sub1 && test_commit -C sub1 initial && From 5cd4d0d8500c6ef1b102f5cb35187a91c299f013 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 2 Jun 2026 07:37:58 +0000 Subject: [PATCH 044/436] config.mak.uname: avoid macOS linker warning on Xcode 16.3+ Building on macOS with Xcode 16.3 or newer emits: ld: warning: reducing alignment of section __DATA,__common from 0x8000 to 0x4000 because it exceeds segment maximum alignment Pass -fno-common when "ld -v" reports ld-1167 or newer, so tentative definitions of large arrays go into BSS instead of __DATA,__common. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- config.mak.uname | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config.mak.uname b/config.mak.uname index 3c35ae33a3c0c0..32b58e7a95091e 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -160,6 +160,12 @@ ifeq ($(uname_S),Darwin) NEEDS_GOOD_LIBICONV = UnfortunatelyYes endif + # Silence Xcode 16.3+ linker warning about __DATA,__common alignment. + LD_MAJOR_VERSION = $(shell ld -v 2>&1 | sed -n 's/.*PROJECT:ld-\([0-9]*\).*/\1/p') + ifeq ($(shell test -n "$(LD_MAJOR_VERSION)" && test "$(LD_MAJOR_VERSION)" -ge 1167 && echo 1),1) + BASIC_CFLAGS += -fno-common + endif + # The builtin FSMonitor on MacOS builds upon Simple-IPC. Both require # Unix domain sockets and PThreads. ifndef NO_PTHREADS From 4018dc29eea31e4273c0f1b02effe6ee852f3898 Mon Sep 17 00:00:00 2001 From: Luna Schwalbe Date: Tue, 2 Jun 2026 10:17:36 +0200 Subject: [PATCH 045/436] doc: document and test `@` prefix for raw timestamps The Git internal date format ` ` fails to parse when the timestamp is less than 100,000,000 (fewer than 9 digits). This happens to avoid potential ambiguity with other date formats such as `YYYYMMDD`, especially when used with approxidate. To force the parser to interpret the value as a raw timestamp, it must be prefixed with `@` (e.g., `@0 +0000`). This behavior was introduced in 2c733fb24c10a9d7aacc51f956bf9b7881980870 (parse_date(): '@' prefix forces git-timestamp, 2012-02-02) but was never documented. Document the `@` prefix in `Documentation/date-formats.adoc` to make this behavior explicit. Also add test cases to `t/t0006-date.sh` to verify and demonstrate the difference between prefixed and unprefixed small timestamps (e.g., `@2000` vs `2000`). Signed-off-by: Luna Schwalbe Co-authored-by: Junio C Hamano Signed-off-by: Junio C Hamano --- Documentation/date-formats.adoc | 5 +++++ t/t0006-date.sh | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/Documentation/date-formats.adoc b/Documentation/date-formats.adoc index e24517c496fce4..330424b2baccda 100644 --- a/Documentation/date-formats.adoc +++ b/Documentation/date-formats.adoc @@ -9,6 +9,11 @@ Git internal format:: `` is the number of seconds since the UNIX epoch. `` is a positive or negative offset from UTC. For example CET (which is 1 hour ahead of UTC) is `+0100`. ++ +It is safer to prepend the `` with `@` (e.g., +`@0 +0000`), which forces Git to interpret it as a raw timestamp. This +is required for values less than 100,000,000 (which have fewer than 9 +digits) to avoid confusion with other date formats like `YYYYMMDD`. RFC 2822:: The standard date format as described by RFC 2822, for example diff --git a/t/t0006-date.sh b/t/t0006-date.sh index 53ced36df448f1..8b4e1870bf118d 100755 --- a/t/t0006-date.sh +++ b/t/t0006-date.sh @@ -138,6 +138,13 @@ check_parse '1969-12-31 23:59:59 Z' bad check_parse '1969-12-31 23:59:59 +11' bad check_parse '1969-12-31 23:59:59 -11' bad +# pathologically small timestamps requiring `@` prefix +check_parse '@0 +0000' '1970-01-01 00:00:00 +0000' +check_parse '@99999999 +0000' '1973-03-03 09:46:39 +0000' +check_parse '99999999 +0000' bad +check_parse '@100000000 +0000' '1973-03-03 09:46:40 +0000' +check_parse '100000000 +0000' '1973-03-03 09:46:40 +0000' + REQUIRE_64BIT_TIME=HAVE_64BIT_TIME check_parse '2099-12-31 23:59:59' '2099-12-31 23:59:59 +0000' check_parse '2099-12-31 23:59:59 +00' '2099-12-31 23:59:59 +0000' @@ -195,6 +202,10 @@ check_approxidate '6AM, June 7, 2009' '2009-06-07 06:00:00' check_approxidate '2008-12-01' '2008-12-01 19:20:00' check_approxidate '2009-12-01' '2009-12-01 19:20:00' +# ambiguous raw timestamp +check_approxidate '2000 +0000' '2000-08-30 19:20:00' +check_approxidate '@2000 +0000' '1970-01-01 00:33:20' + check_date_format_human() { t=$(($GIT_TEST_DATE_NOW - $1)) echo "$t -> $2" >expect From 66ebad2775a1e3904f724522519a7290cb8d9709 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 2 Jun 2026 23:43:03 +0900 Subject: [PATCH 046/436] SubmittingPatches: separate typofixes section The existing text said something about tests (with [[tests]] to make it easier to refer to it from elsewhere) and then flowed into a different topic of typofixes, but it was unclear where the latter started. Add a similar [[typofixes]] marker to the document. Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index d570184ec84998..dec8aea4cb0cbd 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -237,6 +237,7 @@ Do not forget to update the documentation to describe the updated behavior and make sure that the resulting documentation set formats well (try the Documentation/doc-diff script). +[[typofixes]] We currently have a liberal mixture of US and UK English norms for spelling and grammar, which is somewhat unfortunate. A huge patch that touches the files all over the place only to correct the inconsistency From bc58f1c7347a38175782b5a745443f109773a501 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 2 Jun 2026 23:43:04 +0900 Subject: [PATCH 047/436] SubmittingPatches: describe cover letter We talk about how a commit log message should look like, but do not give advice on writing the cover letter to sell a series to the widest possible audience. Helped-by: Patrick Steinhardt Helped-by: Derrick Stolee Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index dec8aea4cb0cbd..df9f722bfeed8d 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -472,6 +472,30 @@ highlighted above. Only capitalize the very first letter of the trailer, i.e. favor "Signed-off-by" over "Signed-Off-By" and "Acked-by:" over "Acked-By". +[[cover-letter]] +=== Cover Letter + +The purpose of your cover letter is to sell your changes, explain what +they are about, and get your target audience interested enough to read +the patches. + +. Every code change comes with risk of regression and maintenance cost. + The cover letter should clearly communicate why the value of your + proposed change is worth applying. You can also describe how the risk + is reduced by the design choices you made while writing the patches. + +. Make sure your target audience can understand what the patches are + about and why they are needed without prior context. + +. For a second or subsequent iteration of the same topic, make sure + people who missed the earlier discussion can still understand what + the patches are about, so they can judge if the topic is worth their + time to read and comment on. + +. To help those who are familiar with earlier iterations, give a + summary of changes since the previous rounds. + + [[ai]] === Use of Artificial Intelligence (AI) From d4800b7a6e8eef7de8c419ffc55c631c62a902de Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:16 -0400 Subject: [PATCH 048/436] git-gui: guard set/unset of GIT_DIR and GIT_WORK_TREE git-gui unconditionally exports _gitdir as GIT_DIR, and _gitworktree as GIT_WORK_TREE, to the environment, and unconditionally unsets these environment variables before invoking gitk or git-gui when a submodule is involved. This export happens even if _gitworktree is empty, which happens when running from a bare repository. However, exporting GIT_WORK_TREE as empty is never valid, and causes errors in git. GIT_DIR must be exported if the repository is not discoverable from the worktree (or current directory if there is no worktree). The user might have configured this. If there is a worktree, git-gui makes this the current directory. However, if the repository sets core.worktree, this value can only be overridden by GIT_WORK_TREE so the latter must be exported. As we cannot eliminate conditions where either variable is needed, let's implement a pair of functions to set / unset these variables without error, and without ever exporting an empty GIT_WORK_TREE. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index 52897fbd099818..e7a87fc9962eda 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1125,6 +1125,20 @@ unset argv0dir ## ## repository setup +proc set_gitdir_vars {} { + global _gitdir _gitworktree env + set env(GIT_DIR) $_gitdir + if {$_gitworktree ne {}} { + set env(GIT_WORK_TREE) $_gitworktree + } +} + +proc unset_gitdir_vars {} { + global env + catch {unset env(GIT_DIR)} + catch {unset env(GIT_WORK_TREE)} +} + set picked 0 if {[catch { set _gitdir $env(GIT_DIR) @@ -1210,8 +1224,8 @@ if {[lindex $_reponame end] eq {.git}} { set _reponame [lindex $_reponame end] } -set env(GIT_DIR) $_gitdir -set env(GIT_WORK_TREE) $_gitworktree +# Export the final paths +set_gitdir_vars ###################################################################### ## @@ -2010,7 +2024,6 @@ proc incr_font_size {font {amt 1}} { proc do_gitk {revs {is_submodule false}} { global current_diff_path file_states current_diff_side ui_index - global _gitdir _gitworktree # -- Always start gitk through whatever we were loaded with. This # lets us bypass using shell process on Windows systems. @@ -2020,8 +2033,6 @@ proc do_gitk {revs {is_submodule false}} { if {$exe eq {}} { error_popup [mc "Couldn't find gitk in PATH"] } else { - global env - set pwd [pwd] if {$is_submodule} { @@ -2049,13 +2060,11 @@ proc do_gitk {revs {is_submodule false}} { # TODO we could make life easier (start up faster?) for gitk # by setting these to the appropriate values to allow gitk # to skip the heuristics to find their proper value - unset env(GIT_DIR) - unset env(GIT_WORK_TREE) + unset_gitdir_vars } safe_exec_bg [concat $cmd $revs "--" "--"] - set env(GIT_DIR) $_gitdir - set env(GIT_WORK_TREE) $_gitworktree + set_gitdir_vars cd $pwd if {[info exists main_status]} { @@ -2078,21 +2087,16 @@ proc do_git_gui {} { if {$exe eq {}} { error_popup [mc "Couldn't find git gui in PATH"] } else { - global env - global _gitdir _gitworktree - # see note in do_gitk about unsetting these vars when # running tools in a submodule - unset env(GIT_DIR) - unset env(GIT_WORK_TREE) + unset_gitdir_vars set pwd [pwd] cd $current_diff_path safe_exec_bg [concat $exe gui] - set env(GIT_DIR) $_gitdir - set env(GIT_WORK_TREE) $_gitworktree + set_gitdir_vars cd $pwd set status_operation [$::main_status \ From 587a4ac448e3c0393f78770c48e85b5e90597470 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:17 -0400 Subject: [PATCH 049/436] git-gui: do not change global vars in choose_repository::pick The repository picker (choose_repository::pick, AKA pick) on success always returns with the current directory at the root of the selected worktree, with the global variable _gitdir holding the name of the git repository, possibly as a relative path, and _prefix {}. The worktree root (_gitworktree) is not filled out, and if the selection was from the "recent" list, no validation has occurred beyond testing that the worktree root exists. So, repository and worktree validation are still needed to be sure the new repo + worktree is usable. pick only supports worktrees with a .git entry in the worktree root, so git repository and worktree discovery will work starting in the current directory on return. In cases of error, or user abort, pick exits the process rather than returning. So, let's change pick to not alter any global values, with success indicated by the process returning to the caller. In this case, the current directory is the worktree root, with a .git entry. The caller then proceeds with normal discovery to find and validate both repository and worktree. With this, pick now returns 1 in the success case, but additional work would be necessary to return from conditions where 0 should be returned. Checking this return value would be superfluous. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 7 ++++++- lib/choose_repository.tcl | 21 ++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index e7a87fc9962eda..efa745dc48fb63 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1153,9 +1153,14 @@ if {[catch { load_config 1 apply_config choose_repository::pick - if {![file isdirectory $_gitdir]} { + if {[catch { + set _gitdir [git rev-parse --git-dir] + } err]} { + catch {wm withdraw .} + error_popup [strcat [mc "Unusable repo/worktree:"] " [pwd] \n\n$err"] exit 1 } + set _prefix {} set picked 1 } diff --git a/lib/choose_repository.tcl b/lib/choose_repository.tcl index 7e1462a20c59d9..4b06afee93f9a7 100644 --- a/lib/choose_repository.tcl +++ b/lib/choose_repository.tcl @@ -15,7 +15,7 @@ field w_recentlist ; # Listbox containing recent repositories field w_localpath ; # Entry widget bound to local_path field done 0 ; # Finished picking the repository? -field clone_ok false ; # clone succeeeded +field pick_ok 0 ; # true if repo pick/clone succeeded field local_path {} ; # Where this repository is locally field origin_url {} ; # Where we are cloning from field origin_name origin ; # What we shall call 'origin' @@ -220,6 +220,8 @@ constructor pick {} { if {$top eq {.}} { eval destroy [winfo children $top] } + + return $pick_ok } method _center {} { @@ -327,8 +329,7 @@ method _git_init {} { } _append_recentrepos [pwd] - set ::_gitdir .git - set ::_prefix {} + set pick_ok 1 return 1 } @@ -409,6 +410,7 @@ method _do_new2 {} { if {![_git_init $this]} { return } + set pick_ok 1 set done 1 } @@ -621,7 +623,7 @@ method _do_clone2 {} { } tkwait variable @done - if {!$clone_ok} { + if {!$pick_ok} { error_popup [mc "Clone failed."] return } @@ -632,18 +634,12 @@ method _do_clone2_done {ok} { if {$ok} { if {[catch { cd $local_path - set ::_gitdir .git - set ::_prefix {} _append_recentrepos [pwd] } err]} { set ok 0 } } - if {!$ok} { - set ::_gitdir {} - set ::_prefix {} - } - set clone_ok $ok + set pick_ok $ok set done 1 } @@ -721,8 +717,7 @@ method _do_open2 {} { } _append_recentrepos [pwd] - set ::_gitdir $actualgit - set ::_prefix {} + set pick_ok 1 set done 1 } From 9902c4cc9c466fa96f55865a21ca2f6867d723c0 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:18 -0400 Subject: [PATCH 050/436] git-gui: use --absolute-git-dir git-gui uses git rev-parse --git-dir to get the pathname of the discovered git repository. The returned value can be relative, and is '.' if the current directory is the top of the repository directory itself. git-gui has code to change '.' to [pwd] in this case so that subsequent logic runs. But, git rev-parse supports --absolute-git-dir from fac60b8925 ("rev-parse: add option for absolute or relative path formatting", 2020-12-13), and included in git 2.31. git-gui requires git >= 2.36, so this more useful form is always available. Use --absolute-git-dir to always get an absolute path, avoiding the need for other checks, and delete the now unneeded code to fix a relative _gitdir. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index efa745dc48fb63..84385c09a25de9 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1145,16 +1145,15 @@ if {[catch { set _prefix {} }] && [catch { - # beware that from the .git dir this sets _gitdir to . - # and _prefix to the empty string - set _gitdir [git rev-parse --git-dir] + # beware that from the .git dir this sets _prefix to the empty string + set _gitdir [git rev-parse --absolute-git-dir] set _prefix [git rev-parse --show-prefix] } err]} { load_config 1 apply_config choose_repository::pick if {[catch { - set _gitdir [git rev-parse --git-dir] + set _gitdir [git rev-parse --absolute-git-dir] } err]} { catch {wm withdraw .} error_popup [strcat [mc "Unusable repo/worktree:"] " [pwd] \n\n$err"] @@ -1175,13 +1174,6 @@ if {$hashalgorithm eq "sha1"} { exit 1 } -# we expand the _gitdir when it's just a single dot (i.e. when we're being -# run from the .git dir itself) lest the routines to find the worktree -# get confused -if {$_gitdir eq "."} { - set _gitdir [pwd] -} - if {![file isdirectory $_gitdir]} { catch {wm withdraw .} error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"] From edcf9188beacfda06b66c8e4819314278c4fa457 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:19 -0400 Subject: [PATCH 051/436] git-gui: use rev-parse exclusively to find a repository git-gui attempts to use env(GIT_DIR) directly as the git repository, accepting GIT_DIR if it is a directory. Only if that fails is git rev-parse used to discover the repository. But, this avoids all of git-core's validity checking on a repository, thus possibly deferring an error to a later step, possibly unexpected. Repository validation should be part of initial setup so that later processing does not need error trapping for configuration errors. Let's just invoke rev-parse so all error checking is done. While here, let's cleanup the error handling. Stop if an error occurs and the user set GIT_DIR or GIT_WORK_TREE. Use of either or both of those variables is supported by git, but their use also means the user has taken responsibility that they are correct, so a failure is something the user must address. Otherwise on error, continue the existing behavior and show the repository picker. But, let's move the possible invocation of repository_chooser::pick to a separate code block. This permits adding separate conditions on using pick independent of repository discovery, and will be exploited later in the series. Note that the picker always returns with the current directory in the root of a worktree with the git repository is in the .git subdirectory. The variable "picked" is used by git-gui to automatically execute the "Explore Working Copy" menu item after the repository picker is run. This is controlled by config variable gui.autoexplore, and happens after all discovery is complete. Remove a later check on whether _gitdir is a directory: that code cannot be reached without rev-parse already validating the repository. _prefix is set as part of worktree discovery, but must be {} if not running with a worktree. Initialze this as {} along with other global variables, this is the correct value is no worktree is found. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index 84385c09a25de9..a057b3f675189e 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -374,6 +374,7 @@ set _gitdir {} set _gitworktree {} set _isbare {} set _githtmldir {} +set _prefix {} set _reponame {} set _shellpath {@@SHELL_PATH@@} @@ -1125,6 +1126,24 @@ unset argv0dir ## ## repository setup +proc is_gitvars_error {err} { + set havevars 0 + set GIT_DIR {} + set GIT_WORK_TREE {} + catch {set GIT_DIR $::env(GIT_DIR); set havevars 1} + catch {set GIT_WORK_TREE $::env(GIT_WORK_TREE); set havevars 1} + + if {$havevars} { + catch {wm withdraw .} + error_popup [strcat [mc "Invalid configuration:"] \ + "\n" "GIT_DIR: " $GIT_DIR \ + "\n" "GIT_WORK_TREE: " $GIT_WORK_TREE \ + "\n\n$err"] + return 1 + } + return 0 +} + proc set_gitdir_vars {} { global _gitdir _gitworktree env set env(GIT_DIR) $_gitdir @@ -1139,16 +1158,22 @@ proc unset_gitdir_vars {} { catch {unset env(GIT_WORK_TREE)} } +# find repository +set _gitdir {} +if {$_gitdir eq {}} { + if {[catch { + set _gitdir [git rev-parse --absolute-git-dir] + } err]} { + if {[is_gitvars_error $err]} { + exit 1 + } + set _gitdir {} + } +} + set picked 0 -if {[catch { - set _gitdir $env(GIT_DIR) - set _prefix {} - }] - && [catch { - # beware that from the .git dir this sets _prefix to the empty string - set _gitdir [git rev-parse --absolute-git-dir] - set _prefix [git rev-parse --show-prefix] - } err]} { +if {$_gitdir eq {}} { + unset_gitdir_vars load_config 1 apply_config choose_repository::pick @@ -1159,7 +1184,6 @@ if {[catch { error_popup [strcat [mc "Unusable repo/worktree:"] " [pwd] \n\n$err"] exit 1 } - set _prefix {} set picked 1 } @@ -1174,11 +1198,6 @@ if {$hashalgorithm eq "sha1"} { exit 1 } -if {![file isdirectory $_gitdir]} { - catch {wm withdraw .} - error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"] - exit 1 -} # _gitdir exists, so try loading the config load_config 0 apply_config From 80b7207e68ffaf56077d9b7d00eb31490fb48cc5 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:20 -0400 Subject: [PATCH 052/436] git-gui: use git rev-parse for worktree discovery git gui uses a combination of tcl code and git invocations to determine the worktree and the location with respect to the worktree root (_prefix). But, git rev-parse provides all of this information directly, and assures full error and configuration checking are done by git itself. The entirety of discovery in normal configurations involves git rev-parse --show-toplevel (gets worktree root) git rev-parse --show-prefix (shows location wrt the root) An error thrown on either of these lines means the worktree discovered by git is unusable, or git did not discover a worktree because the current directory is inside the repository. If the user has defined GIT_DIR or GIT_WORK_TREE, this is a user configuration error and git-gui should stop. Otherwise, the blame or browser subcommands can be used without a worktree. A separate error might occur when changing to the root of the discovered worktree. The cause would be file system related and completely outside of git's control, so trap that independently. Discovery of the repository and the worktree must be guarded to trap errors: the intent is that any configuration problems are caught during discovery, and later processing need not include error trapping and recovery. So, move all worktree discovery code to be immediately after repository discovery. This does move configuration loading to occur after worktree discovery rather than before. None of the code executed in worktree discovery has any option controlled by a git-gui configuration variable, so no impact is expected. git itself will always read the repository configuration, including worktree specific configuration data if that exists, so this is unaffected by when git-gui loads its own config data. Also, we cannot be sure the worktree dependent configuration can be loaded before full discovery is complete. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 61 ++++++++++++++++++++++++------------------------------ 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index a057b3f675189e..66379f290f99d8 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1187,6 +1187,33 @@ if {$_gitdir eq {}} { set picked 1 } +# find worktree, continue without if not required +if {[catch { + set _gitworktree [git rev-parse --show-toplevel] + set _prefix [git rev-parse --show-prefix] + } err]} { + if {[is_gitvars_error $err]} { + exit 1 + } + set _gitworktree {} + set _prefix {} +} + +if {![is_bare]} { + if {[catch {cd $_gitworktree} err]} { + catch {wm withdraw .} + error_popup [strcat [mc "No working directory"] " $_gitworktree:\n\n$err"] + exit 1 + } +} elseif {![is_enabled bare]} { + catch {wm withdraw .} + error_popup [strcat [mc "Cannot use bare repository:"] "\n\n$_gitdir"] + exit 1 +} + +# repository and worktree config are complete, export them +set_gitdir_vars + # Use object format as hash algorithm (either "sha1" or "sha256") set hashalgorithm [git rev-parse --show-object-format] if {$hashalgorithm eq "sha1"} { @@ -1202,37 +1229,6 @@ if {$hashalgorithm eq "sha1"} { load_config 0 apply_config -set _gitworktree [git rev-parse --show-toplevel] - -if {$_prefix ne {}} { - if {$_gitworktree eq {}} { - regsub -all {[^/]+/} $_prefix ../ cdup - } else { - set cdup $_gitworktree - } - if {[catch {cd $cdup} err]} { - catch {wm withdraw .} - error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"] - exit 1 - } - set _gitworktree [pwd] - unset cdup -} elseif {![is_enabled bare]} { - if {[is_bare]} { - catch {wm withdraw .} - error_popup [strcat [mc "Cannot use bare repository:"] "\n\n$_gitdir"] - exit 1 - } - if {$_gitworktree eq {}} { - set _gitworktree [file dirname $_gitdir] - } - if {[catch {cd $_gitworktree} err]} { - catch {wm withdraw .} - error_popup [strcat [mc "No working directory"] " $_gitworktree:\n\n$err"] - exit 1 - } - set _gitworktree [pwd] -} set _reponame [file split [file normalize $_gitdir]] if {[lindex $_reponame end] eq {.git}} { set _reponame [lindex $_reponame end-1] @@ -1240,9 +1236,6 @@ if {[lindex $_reponame end] eq {.git}} { set _reponame [lindex $_reponame end] } -# Export the final paths -set_gitdir_vars - ###################################################################### ## ## global init From d0e9b4959bea9f4bfca2628eb24136b23c00e8fa Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:21 -0400 Subject: [PATCH 053/436] git-gui: simplify [is_bare] to report if a worktree is known git-gui includes proc is_bare, used in several places to make decisions on whether a worktree exists, but also in discovery to tell if a worktree can be supported. But, is_bare is out of date with regard to multiple worktrees, safe repository guards, and possibly other relevant features known to git rev-parse. Also, is_bare caches its result on the first call, so is not useful if a later step in the discovery process finds a worktree. So, simplify is_bare to report whether git-gui has a worktree or is working only from a repository. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index 66379f290f99d8..44dcb5ffaf34fb 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -372,7 +372,6 @@ if {[tk windowingsystem] eq "aqua"} { set _appname {Git Gui} set _gitdir {} set _gitworktree {} -set _isbare {} set _githtmldir {} set _prefix {} set _reponame {} @@ -524,29 +523,7 @@ proc get_config {name} { } proc is_bare {} { - global _isbare - global _gitdir - global _gitworktree - - if {$_isbare eq {}} { - if {[catch { - set _bare [git rev-parse --is-bare-repository] - switch -- $_bare { - true { set _isbare 1 } - false { set _isbare 0} - default { throw } - } - }]} { - if {[is_config_true core.bare] - || ($_gitworktree eq {} - && [lindex [file split $_gitdir] end] ne {.git})} { - set _isbare 1 - } else { - set _isbare 0 - } - } - } - return $_isbare + return [expr {$::_gitworktree eq {}}] } ###################################################################### From 4f5114bfacfe0317d383885ffce13731116d6db5 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:22 -0400 Subject: [PATCH 054/436] git-gui: try harder to find worktree from gitdir git-gui, since 87cd09f43e ("git-gui: work from the .git dir", 2010-01-23), has had the intent to allow starting from inside a repository, then switching to the parent directory if that is a valid worktree. This certainly hasn't worked since 2d92ab32fd ("rev-parse: make --show-toplevel without a worktree an error", 2019-11-19) in git, but breaking this git-gui feature was unintentional. There are (at least) 3 cases where the gitdir can tell us where the worktree is, and we would like all to work: - core.worktree is set, and points to a valid worktree. This is already handled by git rev-parse --show-toplevel, even when not in the worktree. There is nothing more to do in this case. - the gitdir is embedded in a worktree as subdirectory .git. The parent is (or at least should be) a valid worktree. This worked long ago. - the gitdir is a worktree specific directory (under /worktrees/worktree_name), within which there is a file "gitdir" pointing to .git in the worktree. git gui never learned to handle this case. Let's handle the latter two cases. Always check that the discovered worktree is valid and points to the already discovered gitdir according to git rev-parse. This avoids issues that may arise because we are discovering from the gitdir up, rather than the worktree down, and file system non-posix behavior or misconfiguration of git might cause confusion. For instance, a manually moved worktree might not be where the gitdir points, or the gitdir might be configured with core.bare=true. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/git-gui.sh b/git-gui.sh index 44dcb5ffaf34fb..57d48cc8387fb9 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1103,6 +1103,37 @@ unset argv0dir ## ## repository setup +proc find_worktree_from_gitdir {} { + # this is invoked only if the current directory is inside the repository + set worktree {} + if {[file tail $::_gitdir] eq {.git}} { + # the dir containing .git is a worktree if repo allows it + # Check that git reports parent as a worktree (gitdir might not allow a worktree) + if {[catch { + set parent [file dirname $::_gitdir] + set worktree [git -C $parent rev-parse --show-toplevel] + }]} { + set worktree {} + } + } elseif [file exists {gitdir}] { + # a worktree gitdir has .gitdir naming worktree/.git + # assure git run there reports this dir as the gitdir (links might be broken) + if {[catch { + set fd_gitdir [open {gitdir} {r}] + set worktree [file dirname [read $fd_gitdir]] + catch {close $fd_gitdir} + set worktree_gitdir [git -C $worktree rev-parse --absolute-git-dir] + if {$::_gitdir ne $worktree_gitdir} { + set worktree {} + } + }]} { + catch {close $fd_gitdir} + set worktree {} + } + } + return $worktree +} + proc is_gitvars_error {err} { set havevars 0 set GIT_DIR {} @@ -1176,6 +1207,13 @@ if {[catch { set _prefix {} } +if {[is_bare]} { + # Maybe we are in an embedded or worktree specific gitdir + if {[set _gitworktree [find_worktree_from_gitdir]] ne {}} { + set _prefix {} + } +} + if {![is_bare]} { if {[catch {cd $_gitworktree} err]} { catch {wm withdraw .} From 7c7c7ba4cea2f0063efa2a2a0d7c3d05ed8766fb Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:23 -0400 Subject: [PATCH 055/436] git-gui: allow specifying path '.' to the browser Invoking "git-gui browser rev ." should show the file browser for the commitish rev, starting at the current directory. When the current directory is the working tree root, this errors out in normalize_relpath because the '.' is removed, yielding an empty list as argument to [file join ...]. git ls-tree (underlying the browser) accepts '.', so use that as the value when in the root. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/git-gui.sh b/git-gui.sh index 57d48cc8387fb9..40a83487d64a14 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -2992,7 +2992,11 @@ proc normalize_relpath {path} { } lappend elements $item } - return [eval file join $elements] + if {$elements ne {}} { + return [eval file join $elements] + } else { + return {.} + } } # -- Not a normal commit type invocation? Do that instead! From 3fbef193c7e49e8edb5a4421c465df67de944da6 Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:24 -0400 Subject: [PATCH 056/436] git-gui: check browser/blame arguments carefully git gui offers two related commands, browser and blame, that provide graphical interfaces driven by git ls-tree and git blame. As such, the arguments to git-gui need to satisfy those two git commands. But, git-gui does not assure this leading to confusing or incorrect results. For instance 'git browser ' shows a blank browser window rather than error message. Also, commit 3e45ee1ef2 ("git-gui: Smarter command line parsing for browser, blame", 2007-05-08) implemented code to allow giving path before rev on the command line, and unconditionally uses the worktree to disambiguate. As a result, the following command run in a current git-gui checkout of the master branch shows the master branch version of blame.tcl, when none should be shown as that file does not exist in gitgui-0.6.0. git gui blame lib/blame.tcl gitgui-0.6.0 This 'file before rev' feature in git-gui mirrors ideas considered when git's user interface was very young, but no such feature is documented for any git command. Rather than try to fix an idea git itself rejected, let's just remove this broken and hopefully unused feature. git-gui browser|blame both accept 'rev' and 'path' as command line arguments. rev defaults to 'HEAD' if not given, while path must be given. path names a directory tree to ls-tree or a file to blame. path must exist in rev for ls-tree and for blame. In addition git blame will include uncommitted changes from the worktree file at 'path' if rev is not given (thus defaulting to HEAD), but still requires that the file exists in HEAD. So, let's clean up the parser to check that the arguments are usable. - give a full synopsis, including '--' that may be used to separate rev and path. (as path is the required final arg, -- gives no extra info) - explicitly check the number of arguments - use rev-parse to assure a user supplied rev is valid - use ls-tree to assure that path exists in rev - for blame only, with no rev given and a worktree existing, also assure that path points to a file in the worktree With these changes, error messages are thrown by the parser if the path or rev are not known: no blank or erroneous displays are created. Also, this avoids accessing the worktree except in the specific use case supported by blame / git-blame, meaning browser|blame now also work without a worktree. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 122 +++++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index 40a83487d64a14..c72d453586c099 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -2999,101 +2999,105 @@ proc normalize_relpath {path} { } } +proc show_parse_err {err} { + if {[tk windowingsystem] eq "win32"} { + catch {wm withdraw .} + error_popup $err + } else { + puts stderr $err + } + exit 1 +} + # -- Not a normal commit type invocation? Do that instead! # switch -- $subcommand { browser - blame { if {$subcommand eq "blame"} { - set subcommand_args {[--line=] rev? path} + set subcommand_args {[--line=] [rev] [--] } + set required_pathtype blob } else { - set subcommand_args {rev? path} + set subcommand_args {[rev] [--] } + set required_pathtype tree } - if {$argv eq {}} usage + set maxargs [llength $subcommand_args] + set nargs [llength $argv] + if {$nargs < 1 || $nargs > $maxargs} usage set head {} set path {} set jump_spec {} - set is_path 0 - foreach a $argv { - set p [file join $_prefix $a] - if {$is_path || [file exists $p]} { - if {$path ne {}} usage - set path [normalize_relpath $p] - break + set iarg 0 + foreach a $argv { + incr iarg + if {$iarg == $nargs} { + # final argument is path + set path [normalize_relpath [file join $_prefix $a]] } elseif {$a eq {--}} { - if {$path ne {}} { - if {$head ne {}} usage - set head $path - set path {} + # allow before required final arg that must be path + if {$iarg != $nargs - 1} { + usage } - set is_path 1 } elseif {[regexp {^--line=(\d+)$} $a a lnum]} { - if {$jump_spec ne {} || $head ne {}} usage + # --line can only be the first arg + if {$iarg != 1 || $subcommand ne {blame}} usage set jump_spec [list $lnum] } elseif {$head eq {}} { - if {$head ne {}} usage set head $a - set is_path 1 } else { usage } } - unset is_path - if {$head ne {} && $path eq {}} { - if {[string index $head 0] eq {/}} { - set path [normalize_relpath $head] - set head {} + # If head not given, use current branch (HEAD), + # and blame will use worktree if there is one. + set use_worktree 0 + if {$head eq {}} { + load_current_branch + set head $current_branch + if {$subcommand eq {blame} && ![is_bare]} { + if {![file isfile $path]} { + show_parse_err [mc "fatal: no such file '%s' in worktree" $path] + } + set use_worktree 1 + } + } else { + if {[catch { + set commitid \ + [git rev-parse --verify --end-of-options \ + [strcat $head "^{commit}"]] + }]} { + show_parse_err [mc "fatal: '%s' is not a valid rev'" $head] } else { - set path [normalize_relpath $_prefix$head] - set head {} + set current_branch $head } } - if {$head eq {}} { - load_current_branch - } else { - if {[regexp [string map "@@ [expr $hashlength - 1]" {^[0-9a-f]{1,@@}$}] $head]} { - if {[catch { - set head [git rev-parse --verify $head] - } err]} { - if {[tk windowingsystem] eq "win32"} { - tk_messageBox -icon error -title [mc Error] -message $err - } else { - puts stderr $err - } - exit 1 - } + # check path is known in head, and is file / directory as required + set pathtype {} + catch {set pathtype [git ls-tree {--format=%(objecttype)} $head $path]} + if {$pathtype ne {} && $path eq {.}} { + # ls-tree gives contents of root-dir, we need root-dir itself + set pathtype {tree} + } + + if {$pathtype ne $required_pathtype} { + switch -- $required_pathtype { + tree {show_parse_err \ + [mc "'%s' is not a directory in rev '%s'" $path $head]} + blob {show_parse_err \ + [mc "'%s' is not a filename in rev '%s'" $path $head]} } - set current_branch $head } wm deiconify . switch -- $subcommand { browser { - if {$jump_spec ne {}} usage - if {$head eq {}} { - if {$path ne {} && [file isdirectory $path]} { - set head $current_branch - } else { - set head $path - set path {} - } - } browser::new $head $path } blame { - if {$head eq {} && ![file exists $path]} { - catch {wm withdraw .} - tk_messageBox \ - -icon error \ - -type ok \ - -title [mc "git-gui: fatal error"] \ - -message [mc "fatal: cannot stat path %s: No such file or directory" $path] - exit 1 - } - blame::new $head $path $jump_spec + blame::new [expr {$use_worktree ? {} : $head}] $path $jump_spec } } return From e61190e2faf7ee677424c622881899a10e41612c Mon Sep 17 00:00:00 2001 From: Mark Levedahl Date: Sun, 31 May 2026 19:02:25 -0400 Subject: [PATCH 057/436] git-gui: add gui and pick as explicit subcommands git-gui accepts subcommands blame | browser | citool, and assumes the subcommand is 'gui' if none is actually given, But, git-gui also has a repository picker (choose_repository::pick) that can create a new repository + worktree, or choose an existing one, switch to that, and the run the gui. The user has no direct control over invoking the picker, instead the picker is triggered by failure in the repository / worktree discovery process: this includes being started in a directory not controlled by git, which is probably the intended use case. The picker can appear when the user has no intention of creating a new worktree, and the user cannot use the picker to create a new worktree inside another. So, add two explicit subcommands: gui - Run the gui if repository/worktree discovery succeeds, or die with an error message, but never run the picker. pick - First run the picker, regardless, then start the gui in the chosen worktree. Nothing in this changes the prior behavior, the alternates above must be explicitly selected to see any change. Signed-off-by: Mark Levedahl Signed-off-by: Johannes Sixt --- git-gui.sh | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/git-gui.sh b/git-gui.sh index c72d453586c099..15dd2b3a84ccd1 100755 --- a/git-gui.sh +++ b/git-gui.sh @@ -1024,6 +1024,8 @@ proc load_config {include_global} { ## ## feature option selection +enable_option picker +enable_option gitdir_discovery if {[regexp {^git-(.+)$} [file tail $argv0] _junk subcommand]} { unset _junk } else { @@ -1035,6 +1037,9 @@ if {$subcommand eq {gui.sh}} { if {$subcommand eq {gui} && [llength $argv] > 0} { set subcommand [lindex $argv 0] set argv [lrange $argv 1 end] + if {$subcommand eq {gui}} { + disable_option picker + } } enable_option multicommit @@ -1050,6 +1055,7 @@ blame { disable_option multicommit disable_option branch disable_option transport + disable_option picker } citool { enable_option singlecommit @@ -1058,6 +1064,7 @@ citool { disable_option multicommit disable_option branch disable_option transport + disable_option picker while {[llength $argv] > 0} { set a [lindex $argv 0] @@ -1080,6 +1087,9 @@ citool { set argv [lrange $argv 1 end] } } +pick { + disable_option gitdir_discovery +} } ###################################################################### @@ -1168,7 +1178,7 @@ proc unset_gitdir_vars {} { # find repository set _gitdir {} -if {$_gitdir eq {}} { +if {[is_enabled gitdir_discovery]} { if {[catch { set _gitdir [git rev-parse --absolute-git-dir] } err]} { @@ -1180,7 +1190,7 @@ if {$_gitdir eq {}} { } set picked 0 -if {$_gitdir eq {}} { +if {$_gitdir eq {} && [is_enabled picker]} { unset_gitdir_vars load_config 1 apply_config @@ -1195,6 +1205,12 @@ if {$_gitdir eq {}} { set picked 1 } +if {$_gitdir eq {}} { + catch {wm withdraw .} + error_popup [strcat [mc "Git directory not found:"] "\n\n$err"] + exit 1 +} + # find worktree, continue without if not required if {[catch { set _gitworktree [git rev-parse --show-toplevel] @@ -3103,14 +3119,15 @@ blame { return } citool - -gui { +gui - +pick { if {[llength $argv] != 0} { usage } # fall through to setup UI for commits } default { - set err "[mc usage:] $argv0 \[{blame|browser|citool}\]" + set err "[mc usage:] $argv0 \[{blame|browser|citool|gui|pick}\]" if {[tk windowingsystem] eq "win32"} { wm withdraw . tk_messageBox -icon error -message $err \ From 18684282df3e05db3ed9b3cdafc86c97285e4fd4 Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:14 +0100 Subject: [PATCH 058/436] environment: move "trust_ctime" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.trustctime` configuration is currently stored in the global variable `trust_ctime`, which makes it shared across repository instances in a single process. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. `core.trustctime` is parsed eagerly because it is used in low‑level stat‑matching functions (`match_stat_data()`), where a lazy parse could cause unexpected fatal errors, result in a performance regression and complicate libification efforts. This preserves that behavior while tying the value to the repository from which it was read, avoiding cross‑repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use repo_config_values(). Mentored-by: Christian Couder Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- environment.c | 4 ++-- environment.h | 2 +- statinfo.c | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/environment.c b/environment.c index fc3ed8bb1c7a66..0a9067729e5025 100644 --- a/environment.c +++ b/environment.c @@ -42,7 +42,6 @@ static int pack_compression_seen; static int zlib_compression_seen; int trust_executable_bit = 1; -int trust_ctime = 1; int check_stat = 1; int has_symlinks = 1; int minimum_abbrev = 4, default_abbrev = -1; @@ -309,7 +308,7 @@ int git_default_core_config(const char *var, const char *value, return 0; } if (!strcmp(var, "core.trustctime")) { - trust_ctime = git_config_bool(var, value); + cfg->trust_ctime = git_config_bool(var, value); return 0; } if (!strcmp(var, "core.checkstat")) { @@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->attributes_file = NULL; cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; + cfg->trust_ctime = 1; } diff --git a/environment.h b/environment.h index 123a71cdc8d14e..64d537686eed17 100644 --- a/environment.h +++ b/environment.h @@ -91,6 +91,7 @@ struct repo_config_values { /* section "core" config values */ char *attributes_file; int apply_sparse_checkout; + int trust_ctime; /* section "branch" config values */ enum branch_track branch_track; @@ -161,7 +162,6 @@ extern char *git_work_tree_cfg; /* Environment bits from configuration mechanism */ extern int trust_executable_bit; -extern int trust_ctime; extern int check_stat; extern int has_symlinks; extern int minimum_abbrev, default_abbrev; diff --git a/statinfo.c b/statinfo.c index 30a164b0e68cf8..4fc12053f40b20 100644 --- a/statinfo.c +++ b/statinfo.c @@ -3,6 +3,7 @@ #include "git-compat-util.h" #include "environment.h" #include "statinfo.h" +#include "repository.h" /* * Munge st_size into an unsigned int. @@ -63,17 +64,18 @@ void fake_lstat_data(const struct stat_data *sd, struct stat *st) int match_stat_data(const struct stat_data *sd, struct stat *st) { int changed = 0; + struct repo_config_values *cfg = repo_config_values(the_repository); if (sd->sd_mtime.sec != (unsigned int)st->st_mtime) changed |= MTIME_CHANGED; - if (trust_ctime && check_stat && + if (cfg->trust_ctime && check_stat && sd->sd_ctime.sec != (unsigned int)st->st_ctime) changed |= CTIME_CHANGED; #ifdef USE_NSEC if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st)) changed |= MTIME_CHANGED; - if (trust_ctime && check_stat && + if (cfg->trust_ctime && check_stat && sd->sd_ctime.nsec != ST_CTIME_NSEC(*st)) changed |= CTIME_CHANGED; #endif From 88505ed63711eca184409dfd949437c7e41f994e Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:15 +0100 Subject: [PATCH 059/436] environment: move "check_stat" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.checkstat` configuration is currently stored in the global variable `check_stat`, which makes it shared across repository instances within a single process. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. `core.checkstat` is parsed eagerly because it controls how `match_stat_data()` and related functions decide file freshness; a lazy parse could lead to unexpected behavior or complicate libification. This preserves the existing eager‑parsing behavior while tying the value to the repository it was read from, avoiding cross‑repository state leakage, and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- entry.c | 3 ++- environment.c | 6 +++--- environment.h | 2 +- statinfo.c | 10 +++++----- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/entry.c b/entry.c index 7817aee362ed9e..c55e867d8a2bca 100644 --- a/entry.c +++ b/entry.c @@ -443,7 +443,8 @@ static int check_path(const char *path, int len, struct stat *st, int skiplen) static void mark_colliding_entries(const struct checkout *state, struct cache_entry *ce, struct stat *st) { - int trust_ino = check_stat; + struct repo_config_values *cfg = repo_config_values(the_repository); + int trust_ino = cfg->check_stat; #if defined(GIT_WINDOWS_NATIVE) || defined(__CYGWIN__) trust_ino = 0; diff --git a/environment.c b/environment.c index 0a9067729e5025..8542ac31413d5b 100644 --- a/environment.c +++ b/environment.c @@ -42,7 +42,6 @@ static int pack_compression_seen; static int zlib_compression_seen; int trust_executable_bit = 1; -int check_stat = 1; int has_symlinks = 1; int minimum_abbrev = 4, default_abbrev = -1; int ignore_case; @@ -315,9 +314,9 @@ int git_default_core_config(const char *var, const char *value, if (!value) return config_error_nonbool(var); if (!strcasecmp(value, "default")) - check_stat = 1; + cfg->check_stat = 1; else if (!strcasecmp(value, "minimal")) - check_stat = 0; + cfg->check_stat = 0; else return error(_("invalid value for '%s': '%s'"), var, value); @@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->apply_sparse_checkout = 0; cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; + cfg->check_stat = 1; } diff --git a/environment.h b/environment.h index 64d537686eed17..1d3e2e4f230a15 100644 --- a/environment.h +++ b/environment.h @@ -92,6 +92,7 @@ struct repo_config_values { char *attributes_file; int apply_sparse_checkout; int trust_ctime; + int check_stat; /* section "branch" config values */ enum branch_track branch_track; @@ -162,7 +163,6 @@ extern char *git_work_tree_cfg; /* Environment bits from configuration mechanism */ extern int trust_executable_bit; -extern int check_stat; extern int has_symlinks; extern int minimum_abbrev, default_abbrev; extern int ignore_case; diff --git a/statinfo.c b/statinfo.c index 4fc12053f40b20..5e00af127d657d 100644 --- a/statinfo.c +++ b/statinfo.c @@ -68,19 +68,19 @@ int match_stat_data(const struct stat_data *sd, struct stat *st) if (sd->sd_mtime.sec != (unsigned int)st->st_mtime) changed |= MTIME_CHANGED; - if (cfg->trust_ctime && check_stat && + if (cfg->trust_ctime && cfg->check_stat && sd->sd_ctime.sec != (unsigned int)st->st_ctime) changed |= CTIME_CHANGED; #ifdef USE_NSEC - if (check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st)) + if (cfg->check_stat && sd->sd_mtime.nsec != ST_MTIME_NSEC(*st)) changed |= MTIME_CHANGED; - if (cfg->trust_ctime && check_stat && + if (cfg->trust_ctime && cfg->check_stat && sd->sd_ctime.nsec != ST_CTIME_NSEC(*st)) changed |= CTIME_CHANGED; #endif - if (check_stat) { + if (cfg->check_stat) { if (sd->sd_uid != (unsigned int) st->st_uid || sd->sd_gid != (unsigned int) st->st_gid) changed |= OWNER_CHANGED; @@ -94,7 +94,7 @@ int match_stat_data(const struct stat_data *sd, struct stat *st) * clients will have different views of what "device" * the filesystem is on */ - if (check_stat && sd->sd_dev != (unsigned int) st->st_dev) + if (cfg->check_stat && sd->sd_dev != (unsigned int) st->st_dev) changed |= INODE_CHANGED; #endif From e0f86540abd22a98c9701d21d06e75fa2c8d34a0 Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:16 +0100 Subject: [PATCH 060/436] environment: move `zlib_compression_level` into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `zlib_compression_level` configuration is currently stored in the global variable `zlib_compression_level`, which makes it shared across repository instances within a single process. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. `zlib_compression_level` is parsed eagerly because it determines compression behaviour for objects and packs – core operations where a lazy parse could lead to unpredictable results and hinder libification. This preserves the existing eager‑parsing behavior while tying the value to the repository it was read from, avoiding cross‑repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- builtin/index-pack.c | 3 ++- diff.c | 3 ++- environment.c | 6 +++--- environment.h | 2 +- http-push.c | 3 ++- object-file.c | 3 ++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/builtin/index-pack.c b/builtin/index-pack.c index ca7784dc2c4969..3942d3e0d04a5b 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -1416,8 +1416,9 @@ static int write_compressed(struct hashfile *f, void *in, unsigned int size) git_zstream stream; int status; unsigned char outbuf[4096]; + struct repo_config_values *cfg = repo_config_values(the_repository); - git_deflate_init(&stream, zlib_compression_level); + git_deflate_init(&stream, cfg->zlib_compression_level); stream.next_in = in; stream.avail_in = size; diff --git a/diff.c b/diff.c index 397e38b41cc6fa..7d17b0bf3f7102 100644 --- a/diff.c +++ b/diff.c @@ -3589,8 +3589,9 @@ static unsigned char *deflate_it(char *data, int bound; unsigned char *deflated; git_zstream stream; + struct repo_config_values *cfg = repo_config_values(the_repository); - git_deflate_init(&stream, zlib_compression_level); + git_deflate_init(&stream, cfg->zlib_compression_level); bound = git_deflate_bound(&stream, size); deflated = xmalloc(bound); stream.next_out = deflated; diff --git a/environment.c b/environment.c index 8542ac31413d5b..5b0e88b65cf420 100644 --- a/environment.c +++ b/environment.c @@ -52,7 +52,6 @@ char *git_commit_encoding; char *git_log_output_encoding; char *apply_default_whitespace; char *apply_default_ignorewhitespace; -int zlib_compression_level = Z_BEST_SPEED; int pack_compression_level = Z_DEFAULT_COMPRESSION; int fsync_object_files = -1; int use_fsync = -1; @@ -377,7 +376,7 @@ int git_default_core_config(const char *var, const char *value, level = Z_DEFAULT_COMPRESSION; else if (level < 0 || level > Z_BEST_COMPRESSION) die(_("bad zlib compression level %d"), level); - zlib_compression_level = level; + cfg->zlib_compression_level = level; zlib_compression_seen = 1; return 0; } @@ -389,7 +388,7 @@ int git_default_core_config(const char *var, const char *value, else if (level < 0 || level > Z_BEST_COMPRESSION) die(_("bad zlib compression level %d"), level); if (!zlib_compression_seen) - zlib_compression_level = level; + cfg->zlib_compression_level = level; if (!pack_compression_seen) pack_compression_level = level; return 0; @@ -721,4 +720,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->branch_track = BRANCH_TRACK_REMOTE; cfg->trust_ctime = 1; cfg->check_stat = 1; + cfg->zlib_compression_level = Z_BEST_SPEED; } diff --git a/environment.h b/environment.h index 1d3e2e4f230a15..93201620afc302 100644 --- a/environment.h +++ b/environment.h @@ -93,6 +93,7 @@ struct repo_config_values { int apply_sparse_checkout; int trust_ctime; int check_stat; + int zlib_compression_level; /* section "branch" config values */ enum branch_track branch_track; @@ -170,7 +171,6 @@ extern int assume_unchanged; extern int warn_on_object_refname_ambiguity; extern char *apply_default_whitespace; extern char *apply_default_ignorewhitespace; -extern int zlib_compression_level; extern int pack_compression_level; extern unsigned long pack_size_limit_cfg; diff --git a/http-push.c b/http-push.c index d143fe28455623..8ac107a56e08be 100644 --- a/http-push.c +++ b/http-push.c @@ -369,13 +369,14 @@ static void start_put(struct transfer_request *request) int hdrlen; ssize_t size; git_zstream stream; + struct repo_config_values *cfg = repo_config_values(the_repository); unpacked = odb_read_object(the_repository->objects, &request->obj->oid, &type, &len); hdrlen = format_object_header(hdr, sizeof(hdr), type, len); /* Set it up */ - git_deflate_init(&stream, zlib_compression_level); + git_deflate_init(&stream, cfg->zlib_compression_level); size = git_deflate_bound(&stream, len + hdrlen); strbuf_grow(&request->buffer.buf, size); request->buffer.posn = 0; diff --git a/object-file.c b/object-file.c index 2acc9522df2daa..7c122ac419829a 100644 --- a/object-file.c +++ b/object-file.c @@ -906,6 +906,7 @@ static int start_loose_object_common(struct odb_source *source, const struct git_hash_algo *algo = source->odb->repo->hash_algo; const struct git_hash_algo *compat = source->odb->repo->compat_hash_algo; int fd; + struct repo_config_values *cfg = repo_config_values(the_repository); fd = create_tmpfile(source->odb->repo, tmp_file, filename); if (fd < 0) { @@ -921,7 +922,7 @@ static int start_loose_object_common(struct odb_source *source, } /* Setup zlib stream for compression */ - git_deflate_init(stream, zlib_compression_level); + git_deflate_init(stream, cfg->zlib_compression_level); stream->next_out = buf; stream->avail_out = buflen; algo->init_fn(c); From 8cd7402accec35c92d9ea8cc10b9d8e2536ef7b5 Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:17 +0100 Subject: [PATCH 061/436] environment: move "pack_compression_level" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `pack_compression_level` configuration is currently stored in the global variable `pack_compression_level`, which makes it shared across repository instances within a single process. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. `pack_compression_level` is parsed eagerly because it influences packfile compression, a core operation where a lazy parse could cause inconsistent behavior and hamper libification. This preserves the existing eager‑parsing behavior while tying the value to the repository from which it was read, avoiding cross‑repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- builtin/fast-import.c | 8 +++++--- builtin/pack-objects.c | 17 ++++++++++------- environment.c | 8 +++++--- environment.h | 2 +- object-file.c | 3 ++- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/builtin/fast-import.c b/builtin/fast-import.c index 82bc6dcc003723..070a5af3e48c92 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -965,6 +965,7 @@ static int store_object( unsigned long hdrlen, deltalen; struct git_hash_ctx c; git_zstream s; + struct repo_config_values *cfg = repo_config_values(the_repository); hdrlen = format_object_header((char *)hdr, sizeof(hdr), type, dat->len); @@ -1005,7 +1006,7 @@ static int store_object( } else delta = NULL; - git_deflate_init(&s, pack_compression_level); + git_deflate_init(&s, cfg->pack_compression_level); if (delta) { s.next_in = delta; s.avail_in = deltalen; @@ -1032,7 +1033,7 @@ static int store_object( if (delta) { FREE_AND_NULL(delta); - git_deflate_init(&s, pack_compression_level); + git_deflate_init(&s, cfg->pack_compression_level); s.next_in = (void *)dat->buf; s.avail_in = dat->len; s.avail_out = git_deflate_bound(&s, s.avail_in); @@ -1115,6 +1116,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark) struct git_hash_ctx c; git_zstream s; struct hashfile_checkpoint checkpoint; + struct repo_config_values *cfg = repo_config_values(the_repository); int status = Z_OK; /* Determine if we should auto-checkpoint. */ @@ -1134,7 +1136,7 @@ static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark) crc32_begin(pack_file); - git_deflate_init(&s, pack_compression_level); + git_deflate_init(&s, cfg->pack_compression_level); hdrlen = encode_in_pack_object_header(out_buf, out_sz, OBJ_BLOB, len); diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index dd2480a73d2edf..8ccbe7e17832cd 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -386,8 +386,9 @@ static unsigned long do_compress(void **pptr, unsigned long size) git_zstream stream; void *in, *out; unsigned long maxsize; + struct repo_config_values *cfg = repo_config_values(the_repository); - git_deflate_init(&stream, pack_compression_level); + git_deflate_init(&stream, cfg->pack_compression_level); maxsize = git_deflate_bound(&stream, size); in = *pptr; @@ -413,8 +414,9 @@ static unsigned long write_large_blob_data(struct odb_read_stream *st, struct ha unsigned char ibuf[1024 * 16]; unsigned char obuf[1024 * 16]; unsigned long olen = 0; + struct repo_config_values *cfg = repo_config_values(the_repository); - git_deflate_init(&stream, pack_compression_level); + git_deflate_init(&stream, cfg->pack_compression_level); for (;;) { ssize_t readlen; @@ -5003,6 +5005,7 @@ int cmd_pack_objects(int argc, struct string_list keep_pack_list = STRING_LIST_INIT_NODUP; struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT; + struct repo_config_values *cfg = repo_config_values(the_repository); struct option pack_objects_options[] = { OPT_CALLBACK_F('q', "quiet", &progress, NULL, @@ -5084,7 +5087,7 @@ int cmd_pack_objects(int argc, N_("ignore packs that have companion .keep file")), OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"), N_("ignore this pack")), - OPT_INTEGER(0, "compression", &pack_compression_level, + OPT_INTEGER(0, "compression", &cfg->pack_compression_level, N_("pack compression level")), OPT_BOOL(0, "keep-true-parents", &grafts_keep_true_parents, N_("do not hide commits by grafts")), @@ -5243,10 +5246,10 @@ int cmd_pack_objects(int argc, if (!reuse_object) reuse_delta = 0; - if (pack_compression_level == -1) - pack_compression_level = Z_DEFAULT_COMPRESSION; - else if (pack_compression_level < 0 || pack_compression_level > Z_BEST_COMPRESSION) - die(_("bad pack compression level %d"), pack_compression_level); + if (cfg->pack_compression_level == -1) + cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; + else if (cfg->pack_compression_level < 0 || cfg->pack_compression_level > Z_BEST_COMPRESSION) + die(_("bad pack compression level %d"), cfg->pack_compression_level); if (!delta_search_threads) /* --threads=0 means autodetect */ delta_search_threads = online_cpus(); diff --git a/environment.c b/environment.c index 5b0e88b65cf420..d0d3a4b7d29e7e 100644 --- a/environment.c +++ b/environment.c @@ -52,7 +52,6 @@ char *git_commit_encoding; char *git_log_output_encoding; char *apply_default_whitespace; char *apply_default_ignorewhitespace; -int pack_compression_level = Z_DEFAULT_COMPRESSION; int fsync_object_files = -1; int use_fsync = -1; enum fsync_method fsync_method = FSYNC_METHOD_DEFAULT; @@ -390,7 +389,7 @@ int git_default_core_config(const char *var, const char *value, if (!zlib_compression_seen) cfg->zlib_compression_level = level; if (!pack_compression_seen) - pack_compression_level = level; + cfg->pack_compression_level = level; return 0; } @@ -662,6 +661,8 @@ static int git_default_attr_config(const char *var, const char *value) int git_default_config(const char *var, const char *value, const struct config_context *ctx, void *cb) { + struct repo_config_values *cfg = repo_config_values(the_repository); + if (starts_with(var, "core.")) return git_default_core_config(var, value, ctx, cb); @@ -701,7 +702,7 @@ int git_default_config(const char *var, const char *value, level = Z_DEFAULT_COMPRESSION; else if (level < 0 || level > Z_BEST_COMPRESSION) die(_("bad pack compression level %d"), level); - pack_compression_level = level; + cfg->pack_compression_level = level; pack_compression_seen = 1; return 0; } @@ -721,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->trust_ctime = 1; cfg->check_stat = 1; cfg->zlib_compression_level = Z_BEST_SPEED; + cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; } diff --git a/environment.h b/environment.h index 93201620afc302..514576b67a2741 100644 --- a/environment.h +++ b/environment.h @@ -94,6 +94,7 @@ struct repo_config_values { int trust_ctime; int check_stat; int zlib_compression_level; + int pack_compression_level; /* section "branch" config values */ enum branch_track branch_track; @@ -171,7 +172,6 @@ extern int assume_unchanged; extern int warn_on_object_refname_ambiguity; extern char *apply_default_whitespace; extern char *apply_default_ignorewhitespace; -extern int pack_compression_level; extern unsigned long pack_size_limit_cfg; extern int precomposed_unicode; diff --git a/object-file.c b/object-file.c index 7c122ac419829a..37def5cc590784 100644 --- a/object-file.c +++ b/object-file.c @@ -1437,8 +1437,9 @@ static int stream_blob_to_pack(struct transaction_packfile *state, int status = Z_OK; int write_object = (flags & INDEX_WRITE_OBJECT); off_t offset = 0; + struct repo_config_values *cfg = repo_config_values(the_repository); - git_deflate_init(&s, pack_compression_level); + git_deflate_init(&s, cfg->pack_compression_level); hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), OBJ_BLOB, size); s.next_out = obuf + hdrlen; From 6f00fc0499851d33ef6eae3f8633cb67808834aa Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:18 +0100 Subject: [PATCH 062/436] environment: move "precomposed_unicode" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.precomposeunicode` configuration is currently stored in the global variable `precomposed_unicode`, which makes it shared across repository instances within a single process. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. `core.precomposeunicode` is parsed eagerly because it controls Unicode path normalization on macOS, a fundamental filesystem‑level behavior that many operations depend on; a lazy parse could lead to inconsistent results and hamper libification. This preserves the existing behavior while tying the value to the repository from which it was read, avoiding cross‑ repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- compat/precompose_utf8.c | 20 +++++++++++++------- environment.c | 4 ++-- environment.h | 2 +- upload-pack.c | 3 ++- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c index 43b3be011439ef..0e94dbd8629805 100644 --- a/compat/precompose_utf8.c +++ b/compat/precompose_utf8.c @@ -48,16 +48,18 @@ void probe_utf8_pathname_composition(void) static const char *auml_nfc = "\xc3\xa4"; static const char *auml_nfd = "\x61\xcc\x88"; int output_fd; - if (precomposed_unicode != -1) + struct repo_config_values *cfg = repo_config_values(the_repository); + + if (cfg->precomposed_unicode != -1) return; /* We found it defined in the global config, respect it */ repo_git_path_replace(the_repository, &path, "%s", auml_nfc); output_fd = open(path.buf, O_CREAT|O_EXCL|O_RDWR, 0600); if (output_fd >= 0) { close(output_fd); repo_git_path_replace(the_repository, &path, "%s", auml_nfd); - precomposed_unicode = access(path.buf, R_OK) ? 0 : 1; + cfg->precomposed_unicode = access(path.buf, R_OK) ? 0 : 1; repo_config_set(the_repository, "core.precomposeunicode", - precomposed_unicode ? "true" : "false"); + cfg->precomposed_unicode ? "true" : "false"); repo_git_path_replace(the_repository, &path, "%s", auml_nfc); if (unlink(path.buf)) die_errno(_("failed to unlink '%s'"), path.buf); @@ -69,14 +71,16 @@ const char *precompose_string_if_needed(const char *in) { size_t inlen; size_t outlen; + struct repo_config_values *cfg = repo_config_values(the_repository); + if (!in) return NULL; if (has_non_ascii(in, (size_t)-1, &inlen)) { iconv_t ic_prec; char *out; - if (precomposed_unicode < 0) - repo_config_get_bool(the_repository, "core.precomposeunicode", &precomposed_unicode); - if (precomposed_unicode != 1) + if (cfg->precomposed_unicode < 0) + repo_config_get_bool(the_repository, "core.precomposeunicode", &cfg->precomposed_unicode); + if (cfg->precomposed_unicode != 1) return in; ic_prec = iconv_open(repo_encoding, path_encoding); if (ic_prec == (iconv_t) -1) @@ -130,7 +134,9 @@ PREC_DIR *precompose_utf8_opendir(const char *dirname) struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir) { + struct repo_config_values *cfg = repo_config_values(the_repository); struct dirent *res; + res = readdir(prec_dir->dirp); if (res) { size_t namelenz = strlen(res->d_name) + 1; /* \0 */ @@ -149,7 +155,7 @@ struct dirent_prec_psx *precompose_utf8_readdir(PREC_DIR *prec_dir) prec_dir->dirent_nfc->d_ino = res->d_ino; prec_dir->dirent_nfc->d_type = res->d_type; - if ((precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) { + if ((cfg->precomposed_unicode == 1) && has_non_ascii(res->d_name, (size_t)-1, NULL)) { if (prec_dir->ic_precompose == (iconv_t)-1) { die("iconv_open(%s,%s) failed, but needed:\n" " precomposed unicode is not supported.\n" diff --git a/environment.c b/environment.c index d0d3a4b7d29e7e..739b647ebe0ed1 100644 --- a/environment.c +++ b/environment.c @@ -72,7 +72,6 @@ enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE; int grafts_keep_true_parents; int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; -int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ unsigned long pack_size_limit_cfg; #ifndef PROTECT_HFS_DEFAULT @@ -532,7 +531,7 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.precomposeunicode")) { - precomposed_unicode = git_config_bool(var, value); + cfg->precomposed_unicode = git_config_bool(var, value); return 0; } @@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->check_stat = 1; cfg->zlib_compression_level = Z_BEST_SPEED; cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; + cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ } diff --git a/environment.h b/environment.h index 514576b67a2741..508cb1afbc9fda 100644 --- a/environment.h +++ b/environment.h @@ -95,6 +95,7 @@ struct repo_config_values { int check_stat; int zlib_compression_level; int pack_compression_level; + int precomposed_unicode; /* section "branch" config values */ enum branch_track branch_track; @@ -174,7 +175,6 @@ extern char *apply_default_whitespace; extern char *apply_default_ignorewhitespace; extern unsigned long pack_size_limit_cfg; -extern int precomposed_unicode; extern int protect_hfs; extern int protect_ntfs; diff --git a/upload-pack.c b/upload-pack.c index 9f6d6fe48c8c58..3a52237134ef3b 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -1336,6 +1336,7 @@ static int upload_pack_config(const char *var, const char *value, void *cb_data) { struct upload_pack_data *data = cb_data; + struct repo_config_values *cfg = repo_config_values(the_repository); if (!strcmp("uploadpack.allowtipsha1inwant", var)) { if (git_config_bool(var, value)) @@ -1366,7 +1367,7 @@ static int upload_pack_config(const char *var, const char *value, if (value) data->allow_packfile_uris = 1; } else if (!strcmp("core.precomposeunicode", var)) { - precomposed_unicode = git_config_bool(var, value); + cfg->precomposed_unicode = git_config_bool(var, value); } else if (!strcmp("transfer.advertisesid", var)) { data->advertise_sid = git_config_bool(var, value); } From dfa01cee1cb4d5e6c8567828370eb4785f7c33a1 Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:19 +0100 Subject: [PATCH 063/436] environment: move "core_sparse_checkout_cone" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.sparseCheckoutCone` configuration was previously stored in an uninitialized global `int` variable, risking cross‑repository state leakage. Move it into `repo_config_values`, where eagerly‑parsed repository configuration lives. `core.sparseCheckoutCone` is parsed eagerly because it determines the fundamental sparse‑checkout mode and is consulted very early during repository setup; a lazy parse could leave the sparse‑checkout state undefined and complicate libification. This preserves the existing behavior while tying the value to the repository from which it was read, avoiding cross‑ repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- builtin/mv.c | 2 +- builtin/sparse-checkout.c | 37 ++++++++++++++++++++++--------------- dir.c | 3 ++- environment.c | 4 ++-- environment.h | 2 +- sparse-index.c | 2 +- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/builtin/mv.c b/builtin/mv.c index 2215d34e31f29a..ef3a326c906897 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -574,7 +574,7 @@ int cmd_mv(int argc, if (ignore_sparse && cfg->apply_sparse_checkout && - core_sparse_checkout_cone) { + cfg->core_sparse_checkout_cone) { /* * NEEDSWORK: we are *not* paying attention to * "out-to-out" move ( is out-of-cone and diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index f4aa405da93760..92d017b81f9a32 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -73,7 +73,7 @@ static int sparse_checkout_list(int argc, const char **argv, const char *prefix, memset(&pl, 0, sizeof(pl)); - pl.use_cone_patterns = core_sparse_checkout_cone; + pl.use_cone_patterns = cfg->core_sparse_checkout_cone; sparse_filename = get_sparse_checkout_filename(); res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0); @@ -334,6 +334,7 @@ static int write_patterns_and_update(struct repository *repo, FILE *fp; struct lock_file lk = LOCK_INIT; int result; + struct repo_config_values *cfg = repo_config_values(the_repository); sparse_filename = get_sparse_checkout_filename(); @@ -353,7 +354,7 @@ static int write_patterns_and_update(struct repository *repo, if (!fp) die_errno(_("unable to fdopen %s"), get_lock_file_path(&lk)); - if (core_sparse_checkout_cone) + if (cfg->core_sparse_checkout_cone) write_cone_to_file(fp, pl); else write_patterns_to_file(fp, pl); @@ -402,15 +403,15 @@ static enum sparse_checkout_mode update_cone_mode(int *cone_mode) { /* If not specified, use previous definition of cone mode */ if (*cone_mode == -1 && cfg->apply_sparse_checkout) - *cone_mode = core_sparse_checkout_cone; + *cone_mode = cfg->core_sparse_checkout_cone; /* Set cone/non-cone mode appropriately */ cfg->apply_sparse_checkout = 1; if (*cone_mode == 1 || *cone_mode == -1) { - core_sparse_checkout_cone = 1; + cfg->core_sparse_checkout_cone = 1; return MODE_CONE_PATTERNS; } - core_sparse_checkout_cone = 0; + cfg->core_sparse_checkout_cone = 0; return MODE_ALL_PATTERNS; } @@ -577,7 +578,9 @@ static void add_patterns_from_input(struct pattern_list *pl, FILE *file) { int i; - if (core_sparse_checkout_cone) { + struct repo_config_values *cfg = repo_config_values(the_repository); + + if (cfg->core_sparse_checkout_cone) { struct strbuf line = STRBUF_INIT; hashmap_init(&pl->recursive_hashmap, pl_hashmap_cmp, NULL, 0); @@ -636,13 +639,14 @@ static void add_patterns_cone_mode(int argc, const char **argv, struct pattern_entry *pe; struct hashmap_iter iter; struct pattern_list existing; + struct repo_config_values *cfg = repo_config_values(the_repository); char *sparse_filename = get_sparse_checkout_filename(); add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL); memset(&existing, 0, sizeof(existing)); - existing.use_cone_patterns = core_sparse_checkout_cone; + existing.use_cone_patterns = cfg->core_sparse_checkout_cone; if (add_patterns_from_file_to_list(sparse_filename, "", 0, &existing, NULL, 0)) @@ -690,7 +694,7 @@ static int modify_pattern_list(struct repository *repo, switch (m) { case ADD: - if (core_sparse_checkout_cone) + if (cfg->core_sparse_checkout_cone) add_patterns_cone_mode(args->nr, args->v, pl, use_stdin); else add_patterns_literal(args->nr, args->v, pl, use_stdin); @@ -723,11 +727,12 @@ static void sanitize_paths(struct repository *repo, const char *prefix, int skip_checks) { int i; + struct repo_config_values *cfg = repo_config_values(the_repository); if (!args->nr) return; - if (prefix && *prefix && core_sparse_checkout_cone) { + if (prefix && *prefix && cfg->core_sparse_checkout_cone) { /* * The args are not pathspecs, so unfortunately we * cannot imitate how cmd_add() uses parse_pathspec(). @@ -744,10 +749,10 @@ static void sanitize_paths(struct repository *repo, if (skip_checks) return; - if (prefix && *prefix && !core_sparse_checkout_cone) + if (prefix && *prefix && !cfg->core_sparse_checkout_cone) die(_("please run from the toplevel directory in non-cone mode")); - if (core_sparse_checkout_cone) { + if (cfg->core_sparse_checkout_cone) { for (i = 0; i < args->nr; i++) { if (args->v[i][0] == '/') die(_("specify directories rather than patterns (no leading slash)")); @@ -769,7 +774,7 @@ static void sanitize_paths(struct repository *repo, if (S_ISSPARSEDIR(ce->ce_mode)) continue; - if (core_sparse_checkout_cone) + if (cfg->core_sparse_checkout_cone) die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), args->v[i]); else warning(_("pass a leading slash before paths such as '%s' if you want a single file (see NON-CONE PROBLEMS in the git-sparse-checkout manual)."), args->v[i]); @@ -836,6 +841,7 @@ static struct sparse_checkout_set_opts { static int sparse_checkout_set(int argc, const char **argv, const char *prefix, struct repository *repo) { + struct repo_config_values *cfg = repo_config_values(the_repository); int default_patterns_nr = 2; const char *default_patterns[] = {"/*", "!/*/", NULL}; @@ -873,7 +879,7 @@ static int sparse_checkout_set(int argc, const char **argv, const char *prefix, * non-cone mode, if nothing is specified, manually select just the * top-level directory (much as 'init' would do). */ - if (!core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) { + if (!cfg->core_sparse_checkout_cone && !set_opts.use_stdin && argc == 0) { for (int i = 0; i < default_patterns_nr; i++) strvec_push(&patterns, default_patterns[i]); } else { @@ -977,7 +983,7 @@ static int sparse_checkout_clean(int argc, const char **argv, setup_work_tree(); if (!cfg->apply_sparse_checkout) die(_("must be in a sparse-checkout to clean directories")); - if (!core_sparse_checkout_cone) + if (!cfg->core_sparse_checkout_cone) die(_("must be in a cone-mode sparse-checkout to clean directories")); argc = parse_options(argc, argv, prefix, @@ -1141,6 +1147,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char * FILE *fp; int ret; struct pattern_list pl = {0}; + struct repo_config_values *cfg = repo_config_values(the_repository); char *sparse_filename; check_rules_opts.cone_mode = -1; @@ -1152,7 +1159,7 @@ static int sparse_checkout_check_rules(int argc, const char **argv, const char * check_rules_opts.cone_mode = 1; update_cone_mode(&check_rules_opts.cone_mode); - pl.use_cone_patterns = core_sparse_checkout_cone; + pl.use_cone_patterns = cfg->core_sparse_checkout_cone; if (check_rules_opts.rules_file) { fp = xfopen(check_rules_opts.rules_file, "r"); add_patterns_from_input(&pl, argc, argv, fp); diff --git a/dir.c b/dir.c index fcb8f6dd2aa969..4f493b64c68dd8 100644 --- a/dir.c +++ b/dir.c @@ -3508,8 +3508,9 @@ int get_sparse_checkout_patterns(struct pattern_list *pl) { int res; char *sparse_filename = get_sparse_checkout_filename(); + struct repo_config_values *cfg = repo_config_values(the_repository); - pl->use_cone_patterns = core_sparse_checkout_cone; + pl->use_cone_patterns = cfg->core_sparse_checkout_cone; res = add_patterns_from_file_to_list(sparse_filename, "", 0, pl, NULL, 0); free(sparse_filename); diff --git a/environment.c b/environment.c index 739b647ebe0ed1..b0e873e9f5b901 100644 --- a/environment.c +++ b/environment.c @@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED; #endif enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE; int grafts_keep_true_parents; -int core_sparse_checkout_cone; int sparse_expect_files_outside_of_patterns; unsigned long pack_size_limit_cfg; @@ -526,7 +525,7 @@ int git_default_core_config(const char *var, const char *value, } if (!strcmp(var, "core.sparsecheckoutcone")) { - core_sparse_checkout_cone = git_config_bool(var, value); + cfg->core_sparse_checkout_cone = git_config_bool(var, value); return 0; } @@ -723,4 +722,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->zlib_compression_level = Z_BEST_SPEED; cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ + cfg->core_sparse_checkout_cone = 0; } diff --git a/environment.h b/environment.h index 508cb1afbc9fda..befad9a38876e9 100644 --- a/environment.h +++ b/environment.h @@ -96,6 +96,7 @@ struct repo_config_values { int zlib_compression_level; int pack_compression_level; int precomposed_unicode; + int core_sparse_checkout_cone; /* section "branch" config values */ enum branch_track branch_track; @@ -178,7 +179,6 @@ extern unsigned long pack_size_limit_cfg; extern int protect_hfs; extern int protect_ntfs; -extern int core_sparse_checkout_cone; extern int sparse_expect_files_outside_of_patterns; enum rebase_setup_type { diff --git a/sparse-index.c b/sparse-index.c index 13629c075d06e0..53cb8d64fc9b2c 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -154,7 +154,7 @@ int is_sparse_index_allowed(struct index_state *istate, int flags) { struct repo_config_values *cfg = repo_config_values(the_repository); - if (!cfg->apply_sparse_checkout || !core_sparse_checkout_cone) + if (!cfg->apply_sparse_checkout || !cfg->core_sparse_checkout_cone) return 0; if (!(flags & SPARSE_INDEX_MEMORY_ONLY)) { From c8a32140a7d76565a6d14e8d068e2a9b1562ac95 Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:20 +0100 Subject: [PATCH 064/436] environment: move "sparse_expect_files_outside_of_patterns" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.sparseCheckoutExpectFilesOutsideOfPatterns` configuration was previously stored in a global `int` variable, making it shared across repository instances and risking cross‑repository state leakage. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. This option is parsed eagerly because it controls how sparse‑checkout paths are interpreted – a fundamental behavior that many commands rely on; a lazy parse could cause inconsistent sparse‑checkout handling and complicate libification. This preserves the existing behavior while tying the value to the repository from which it was read, avoiding cross‑repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- environment.c | 6 ++++-- environment.h | 5 +++-- sparse-index.c | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/environment.c b/environment.c index b0e873e9f5b901..57587ede56a1be 100644 --- a/environment.c +++ b/environment.c @@ -70,7 +70,6 @@ enum push_default_type push_default = PUSH_DEFAULT_UNSPECIFIED; #endif enum object_creation_mode object_creation_mode = OBJECT_CREATION_MODE; int grafts_keep_true_parents; -int sparse_expect_files_outside_of_patterns; unsigned long pack_size_limit_cfg; #ifndef PROTECT_HFS_DEFAULT @@ -550,8 +549,10 @@ int git_default_core_config(const char *var, const char *value, static int git_default_sparse_config(const char *var, const char *value) { + struct repo_config_values *cfg = repo_config_values(the_repository); + if (!strcmp(var, "sparse.expectfilesoutsideofpatterns")) { - sparse_expect_files_outside_of_patterns = git_config_bool(var, value); + cfg->sparse_expect_files_outside_of_patterns = git_config_bool(var, value); return 0; } @@ -723,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->pack_compression_level = Z_DEFAULT_COMPRESSION; cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ cfg->core_sparse_checkout_cone = 0; + cfg->sparse_expect_files_outside_of_patterns = 0; } diff --git a/environment.h b/environment.h index befad9a38876e9..609cdaa07fc8ca 100644 --- a/environment.h +++ b/environment.h @@ -98,6 +98,9 @@ struct repo_config_values { int precomposed_unicode; int core_sparse_checkout_cone; + /* section "sparse" config values */ + int sparse_expect_files_outside_of_patterns; + /* section "branch" config values */ enum branch_track branch_track; }; @@ -179,8 +182,6 @@ extern unsigned long pack_size_limit_cfg; extern int protect_hfs; extern int protect_ntfs; -extern int sparse_expect_files_outside_of_patterns; - enum rebase_setup_type { AUTOREBASE_NEVER = 0, AUTOREBASE_LOCAL, diff --git a/sparse-index.c b/sparse-index.c index 53cb8d64fc9b2c..1ed769b78d8de1 100644 --- a/sparse-index.c +++ b/sparse-index.c @@ -675,7 +675,7 @@ void clear_skip_worktree_from_present_files(struct index_state *istate) struct repo_config_values *cfg = repo_config_values(the_repository); if (!cfg->apply_sparse_checkout || - sparse_expect_files_outside_of_patterns) + cfg->sparse_expect_files_outside_of_patterns) return; if (clear_skip_worktree_from_present_files_sparse(istate)) { From 8407abf02aa310f4b8c21e0c9da925f8091564ff Mon Sep 17 00:00:00 2001 From: Olamide Caleb Bello Date: Tue, 2 Jun 2026 18:09:21 +0100 Subject: [PATCH 065/436] environment: move "warn_on_object_refname_ambiguity" into `struct repo_config_values` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `core.warnAmbiguousRefs` configuration was previously stored in a global `int` variable, making it shared across repository instances and risking cross‑repository state leakage. Store it instead in `repo_config_values`, where eagerly‑parsed repository configuration lives. This option is parsed eagerly because ambiguity warnings influence how users interpret object references in many commands; a lazy parse could cause these warnings to behave inconsistently or to appear for the wrong repository, confusing users and hindering libification. This preserves the existing behavior while tying the value to the repository from which it was read, avoiding cross‑repository state leakage and continuing the effort to reduce reliance on global configuration state. Update all references to use `repo_config_values()`. Mentored-by: Christian Couder Mentored-by: Usman Akinyemi Signed-off-by: Olamide Caleb Bello Signed-off-by: Junio C Hamano --- builtin/cat-file.c | 7 ++++--- builtin/pack-objects.c | 7 ++++--- environment.c | 2 +- environment.h | 2 +- object-name.c | 3 ++- revision.c | 7 ++++--- submodule.c | 7 ++++--- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/builtin/cat-file.c b/builtin/cat-file.c index d9fbad535868bb..cfc543018684c1 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -901,6 +901,7 @@ static int batch_objects(struct batch_options *opt) struct strbuf input = STRBUF_INIT; struct strbuf output = STRBUF_INIT; struct expand_data data = EXPAND_DATA_INIT; + struct repo_config_values *cfg = repo_config_values(the_repository); int save_warning; int retval = 0; @@ -973,8 +974,8 @@ static int batch_objects(struct batch_options *opt) * warn) ends up dwarfing the actual cost of the object lookups * themselves. We can work around it by just turning off the warning. */ - save_warning = warn_on_object_refname_ambiguity; - warn_on_object_refname_ambiguity = 0; + save_warning = cfg->warn_on_object_refname_ambiguity; + cfg->warn_on_object_refname_ambiguity = 0; if (opt->batch_mode == BATCH_MODE_QUEUE_AND_DISPATCH) { batch_objects_command(opt, &output, &data); @@ -1002,7 +1003,7 @@ static int batch_objects(struct batch_options *opt) cleanup: strbuf_release(&input); strbuf_release(&output); - warn_on_object_refname_ambiguity = save_warning; + cfg->warn_on_object_refname_ambiguity = save_warning; return retval; } diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 8ccbe7e17832cd..7df75fe91e1488 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -4788,6 +4788,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv) struct setup_revision_opt s_r_opt = { .allow_exclude_promisor_objects = 1, }; + struct repo_config_values *cfg = repo_config_values(the_repository); char line[1000]; int flags = 0; int save_warning; @@ -4798,8 +4799,8 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv) /* make sure shallows are read */ is_repository_shallow(the_repository); - save_warning = warn_on_object_refname_ambiguity; - warn_on_object_refname_ambiguity = 0; + save_warning = cfg->warn_on_object_refname_ambiguity; + cfg->warn_on_object_refname_ambiguity = 0; while (fgets(line, sizeof(line), stdin) != NULL) { int len = strlen(line); @@ -4827,7 +4828,7 @@ static void get_object_list(struct rev_info *revs, struct strvec *argv) die(_("bad revision '%s'"), line); } - warn_on_object_refname_ambiguity = save_warning; + cfg->warn_on_object_refname_ambiguity = save_warning; if (use_bitmap_index && !get_object_list_from_bitmap(revs)) return; diff --git a/environment.c b/environment.c index 57587ede56a1be..ba2c60103ff51c 100644 --- a/environment.c +++ b/environment.c @@ -47,7 +47,6 @@ int minimum_abbrev = 4, default_abbrev = -1; int ignore_case; int assume_unchanged; int is_bare_repository_cfg = -1; /* unspecified */ -int warn_on_object_refname_ambiguity = 1; char *git_commit_encoding; char *git_log_output_encoding; char *apply_default_whitespace; @@ -725,4 +724,5 @@ void repo_config_values_init(struct repo_config_values *cfg) cfg->precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */ cfg->core_sparse_checkout_cone = 0; cfg->sparse_expect_files_outside_of_patterns = 0; + cfg->warn_on_object_refname_ambiguity = 1; } diff --git a/environment.h b/environment.h index 609cdaa07fc8ca..1ff0a7ba8b0e82 100644 --- a/environment.h +++ b/environment.h @@ -97,6 +97,7 @@ struct repo_config_values { int pack_compression_level; int precomposed_unicode; int core_sparse_checkout_cone; + int warn_on_object_refname_ambiguity; /* section "sparse" config values */ int sparse_expect_files_outside_of_patterns; @@ -174,7 +175,6 @@ extern int has_symlinks; extern int minimum_abbrev, default_abbrev; extern int ignore_case; extern int assume_unchanged; -extern int warn_on_object_refname_ambiguity; extern char *apply_default_whitespace; extern char *apply_default_ignorewhitespace; extern unsigned long pack_size_limit_cfg; diff --git a/object-name.c b/object-name.c index 21dcdc4a0e7c55..319d3db01da110 100644 --- a/object-name.c +++ b/object-name.c @@ -684,11 +684,12 @@ static int get_oid_basic(struct repository *r, const char *str, int len, int refs_found = 0; int at, reflog_len, nth_prior = 0; int fatal = !(flags & GET_OID_QUIETLY); + struct repo_config_values *cfg = repo_config_values(the_repository); if (len == r->hash_algo->hexsz && !get_oid_hex(str, oid)) { if (!(flags & GET_OID_SKIP_AMBIGUITY_CHECK) && repo_settings_get_warn_ambiguous_refs(r) && - warn_on_object_refname_ambiguity) { + cfg->warn_on_object_refname_ambiguity) { refs_found = repo_dwim_ref(r, str, len, &tmp_oid, &real_ref, 0); if (refs_found > 0) { warning(warn_msg, len, str); diff --git a/revision.c b/revision.c index 599b3a66c369ca..4e7faa7eb15022 100644 --- a/revision.c +++ b/revision.c @@ -2922,9 +2922,10 @@ static void read_revisions_from_stdin(struct rev_info *revs, int seen_end_of_options = 0; int save_warning; int flags = 0; + struct repo_config_values *cfg = repo_config_values(the_repository); - save_warning = warn_on_object_refname_ambiguity; - warn_on_object_refname_ambiguity = 0; + save_warning = cfg->warn_on_object_refname_ambiguity; + cfg->warn_on_object_refname_ambiguity = 0; strbuf_init(&sb, 1000); while (strbuf_getline(&sb, stdin) != EOF) { @@ -2958,7 +2959,7 @@ static void read_revisions_from_stdin(struct rev_info *revs, read_pathspec_from_stdin(&sb, prune); strbuf_release(&sb); - warn_on_object_refname_ambiguity = save_warning; + cfg->warn_on_object_refname_ambiguity = save_warning; } static void NORETURN diagnose_missing_default(const char *def) diff --git a/submodule.c b/submodule.c index b1a0363f9d2a96..f26235bbb728ee 100644 --- a/submodule.c +++ b/submodule.c @@ -898,12 +898,13 @@ static void collect_changed_submodules(struct repository *r, struct setup_revision_opt s_r_opt = { .assume_dashdash = 1, }; + struct repo_config_values *cfg = repo_config_values(the_repository); - save_warning = warn_on_object_refname_ambiguity; - warn_on_object_refname_ambiguity = 0; + save_warning = cfg->warn_on_object_refname_ambiguity; + cfg->warn_on_object_refname_ambiguity = 0; repo_init_revisions(r, &rev, NULL); setup_revisions_from_strvec(argv, &rev, &s_r_opt); - warn_on_object_refname_ambiguity = save_warning; + cfg->warn_on_object_refname_ambiguity = save_warning; if (prepare_revision_walk(&rev)) die(_("revision walk setup failed")); From bb4ce23284d3605c892fdf4fe349fe8773c813d2 Mon Sep 17 00:00:00 2001 From: Mirko Faina Date: Tue, 19 May 2026 02:55:22 +0200 Subject: [PATCH 066/436] revision.c: implement --max-count-oldest "--max-count" is a commit limiting option and sets a maximum amount of commits to be shown. If a user wants to see only the first N commits of the history (the oldest commits) they'd have to do something like git log $(git rev-list HEAD | tail -n N | head -n 1) This is not very user-friendly. Teach get_revision() the --max-count-oldest option. Signed-off-by: Mirko Faina [jc: fixed up t4202 ] Signed-off-by: Junio C Hamano --- Documentation/rev-list-options.adoc | 5 +- revision.c | 111 +++++++++++++++++++++++++++- revision.h | 2 + t/t4202-log.sh | 40 ++++++++++ 4 files changed, 154 insertions(+), 4 deletions(-) diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc index 2d195a147456ea..e8c88d0f1c758f 100644 --- a/Documentation/rev-list-options.adoc +++ b/Documentation/rev-list-options.adoc @@ -16,7 +16,10 @@ ordering and formatting options, such as `--reverse`. `-`:: `-n `:: `--max-count=`:: - Limit the output to __ commits. + Limit the output to the first __ commits that would be shown. + +`--max-count-oldest=`:: + Limit the output to the last __ commits that would be shown. `--skip=`:: Skip __ commits before starting to show the commit output. diff --git a/revision.c b/revision.c index 599b3a66c369ca..5d53db3152ddf0 100644 --- a/revision.c +++ b/revision.c @@ -2339,10 +2339,28 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg } if ((argcount = parse_long_opt("max-count", argv, &optarg))) { + if (revs->max_count_type == 1) + die_for_incompatible_opt2(1, "--max-count", 1, + "--max-count-oldest"); revs->max_count = parse_count(optarg); revs->no_walk = 0; + revs->max_count_type = 0; return argcount; + } else if ((argcount = parse_long_opt("max-count-oldest", argv, &optarg))) { + if (revs->max_count_type == 0 && revs->max_count != -1) + die_for_incompatible_opt2(1, "--max-count", 1, + "--max-count-oldest"); + if (revs->skip_count > 0) + die_for_incompatible_opt2(1, "--skip", 1, + "--max-count-oldest"); + revs->max_count = parse_count(optarg); + revs->no_walk = 0; + revs->max_count_type = 1; + revs->max_count_stage = 0; } else if ((argcount = parse_long_opt("skip", argv, &optarg))) { + if (revs->max_count_type == 1) + die_for_incompatible_opt2(1, "--skip", 1, + "--max-count-oldest"); revs->skip_count = parse_count(optarg); return argcount; } else if ((*arg == '-') && isdigit(arg[1])) { @@ -4521,15 +4539,91 @@ static struct commit *get_revision_internal(struct rev_info *revs) return c; } +static void retrieve_oldest_commits(struct rev_info *revs, + struct commit_list **queue) +{ + struct commit *c; + int max_count = revs->max_count; + int queuei_count = 0; + int queueo_count = 0; + struct commit_list *queueo = NULL; + struct commit_list *queuei = NULL; + struct commit_list *reversed_queue = NULL; + struct commit_list *p; + + revs->max_count = -1; + while ((c = get_revision_internal(revs))) { + /* + * We need to reset SHOWN status otherwise --graph breaks. + * It is fine to do, get_revision_internal() doesn't consider + * children commits as they have been already processed and the + * traversal happens only child to parent. + * + * We do this because the --graph machinery relies on the status + * of the parents to decide how the printing will happen. + * + * We can't simply replace this instruction with a + * graph_update() as it doesn't do the actualy printing, we'd + * have to remove any commit that goes over the + * --max-count-oldest limit from revs->graph. + */ + c->object.flags &= ~(SHOWN | CHILD_SHOWN); + commit_list_insert(c, &queuei); + if (!(c->object.flags & BOUNDARY)) + queuei_count++; + while (queuei_count + queueo_count > max_count) { + if (!queueo_count) { + while ((c = pop_commit(&queuei))) { + commit_list_insert(c, &queueo); + queueo_count++; + } + queuei_count = 0; + } + c = pop_commit(&queueo); + queueo_count--; + /* We need to do this otherwise we'll discard the + * commits that go over the --max-count-oldest limit but + * not their respective boundaries. This matters only if + * we're discarding the commit right before the boundary. + */ + for (p = c->parents; p; p = p->next) + p->item->object.flags &= ~CHILD_SHOWN; + } + } + + while ((c = pop_commit(&queueo))) + commit_list_insert(c, &reversed_queue); + while ((c = pop_commit(&queuei))) + commit_list_insert(c, &queueo); + while ((c = pop_commit(&queueo))) + commit_list_insert(c, &reversed_queue); + + while ((c = pop_commit(&reversed_queue))) + commit_list_insert(c, queue); +} + struct commit *get_revision(struct rev_info *revs) { struct commit *c; struct commit_list *reversed; + struct commit_list *queue = NULL; + struct commit_list *p; + + if (revs->max_count_type == 1 && !revs->max_count_stage) { + retrieve_oldest_commits(revs, &queue); + commit_list_free(revs->commits); + revs->commits = queue; + revs->max_count_stage = 1; + } if (revs->reverse) { reversed = NULL; - while ((c = get_revision_internal(revs))) - commit_list_insert(c, &reversed); + if (revs->max_count_type == 1) + while ((c = pop_commit(&revs->commits))) + commit_list_insert(c, &reversed); + else + while ((c = get_revision_internal(revs))) + commit_list_insert(c, &reversed); commit_list_free(revs->commits); revs->commits = reversed; revs->reverse = 0; @@ -4543,7 +4637,18 @@ struct commit *get_revision(struct rev_info *revs) return c; } - c = get_revision_internal(revs); + if (revs->max_count_stage) { + c = pop_commit(&revs->commits); + if (c) { + c->object.flags |= SHOWN; + if (!(c->object.flags & BOUNDARY)) + for (p = c->parents; p; p = p->next) + p->item->object.flags |= CHILD_SHOWN; + } + } else { + c = get_revision_internal(revs); + } + if (c && revs->graph) graph_update(revs->graph, c); if (!c) { diff --git a/revision.h b/revision.h index 584f1338b5e323..e157463cb1f62c 100644 --- a/revision.h +++ b/revision.h @@ -309,6 +309,8 @@ struct rev_info { /* special limits */ int skip_count; int max_count; + unsigned int max_count_type:1; + unsigned int max_count_stage:1; timestamp_t max_age; timestamp_t max_age_as_filter; timestamp_t min_age; diff --git a/t/t4202-log.sh b/t/t4202-log.sh index 05cee9e41bb48d..75edb0eb38c039 100755 --- a/t/t4202-log.sh +++ b/t/t4202-log.sh @@ -1882,6 +1882,46 @@ test_expect_success 'log --graph with --name-status' ' test_cmp_graph --name-status tangle..reach ' +test_expect_success 'log --max-count-oldest=3 --oneline' ' + test_when_finished rm expect && + git log --oneline | tail -n3 >expect && + git log --oneline --max-count-oldest=3 >actual && + test_cmp expect actual +' + +test_expect_success 'log --max-count-oldest=3 --reverse --oneline' ' + test_when_finished rm expect && + git log --oneline --reverse | head -n3 >expect && + git log --oneline --max-count-oldest=3 --reverse >actual && + test_cmp expect actual +' + +test_expect_success 'log --max-count-oldest with --max-count' ' + test_when_finished rm stderr && + test_must_fail git log --max-count-oldest=3 --max-count=3 2>stderr && + test_grep "cannot be used together" stderr +' + +test_expect_success 'log --max-count-oldest with --skip' ' + test_when_finished rm stderr && + test_must_fail git log --max-count-oldest=3 --skip=1 2>stderr && + test_grep "cannot be used together" stderr +' + +test_expect_success 'log --max-count-oldest=1000 --graph --boundary' ' + test_when_finished rm expect actual && + git log --graph --boundary >expect && + git log --max-count-oldest=1000 --graph --boundary >actual && + test_cmp expect actual +' + +test_expect_success 'log --oneline --graph --boundary --max-count-oldest=1' ' + test_when_finished rm -f actual && + git log --oneline --graph --boundary --max-count-oldest=1 \ + HEAD~1..HEAD >actual && + test_line_count = 2 actual +' + cat >expect <<-\EOF * reach | From 9b03e2790af03bebc9bc084cfc921492e6d5ca70 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 2 Jun 2026 18:43:27 +0000 Subject: [PATCH 067/436] config: add git_config_key_is_valid() for quiet validation Move the body of git_config_parse_key() into a static helper do_parse_config_key() that takes a "quiet" flag and treats store_key as optional. git_config_parse_key() becomes a thin wrapper. Add git_config_key_is_valid() for callers that only need to know whether a key is well-formed. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- config.c | 38 +++++++++++++++++++++++++++++--------- config.h | 2 ++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/config.c b/config.c index 156f2a24fa0027..7ae356ccaa4b3f 100644 --- a/config.c +++ b/config.c @@ -536,11 +536,14 @@ static inline int iskeychar(int c) * -2 if there is no section name in the key. * * store_key - pointer to char* which will hold a copy of the key with - * lowercase section and variable name + * lowercase section and variable name, can be NULL to skip + * allocation when only validation is needed * baselen - pointer to size_t which will hold the length of the * section + subsection part, can be NULL + * quiet - when non-zero, suppress error() reports on rejection */ -int git_config_parse_key(const char *key, char **store_key, size_t *baselen_) +static int do_parse_config_key(const char *key, char **store_key, + size_t *baselen_, int quiet) { size_t i, baselen; int dot; @@ -552,12 +555,14 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_) */ if (last_dot == NULL || last_dot == key) { - error(_("key does not contain a section: %s"), key); + if (!quiet) + error(_("key does not contain a section: %s"), key); return -CONFIG_NO_SECTION_OR_NAME; } if (!last_dot[1]) { - error(_("key does not contain variable name: %s"), key); + if (!quiet) + error(_("key does not contain variable name: %s"), key); return -CONFIG_NO_SECTION_OR_NAME; } @@ -568,7 +573,8 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_) /* * Validate the key and while at it, lower case it for matching. */ - *store_key = xmallocz(strlen(key)); + if (store_key) + *store_key = xmallocz(strlen(key)); dot = 0; for (i = 0; key[i]; i++) { @@ -579,24 +585,38 @@ int git_config_parse_key(const char *key, char **store_key, size_t *baselen_) if (!dot || i > baselen) { if (!iskeychar(c) || (i == baselen + 1 && !isalpha(c))) { - error(_("invalid key: %s"), key); + if (!quiet) + error(_("invalid key: %s"), key); goto out_free_ret_1; } c = tolower(c); } else if (c == '\n') { - error(_("invalid key (newline): %s"), key); + if (!quiet) + error(_("invalid key (newline): %s"), key); goto out_free_ret_1; } - (*store_key)[i] = c; + if (store_key) + (*store_key)[i] = c; } return 0; out_free_ret_1: - FREE_AND_NULL(*store_key); + if (store_key) + FREE_AND_NULL(*store_key); return -CONFIG_INVALID_KEY; } +int git_config_parse_key(const char *key, char **store_key, size_t *baselen_) +{ + return do_parse_config_key(key, store_key, baselen_, 0); +} + +int git_config_key_is_valid(const char *key) +{ + return !do_parse_config_key(key, NULL, NULL, 1); +} + static int config_parse_pair(const char *key, const char *value, struct key_value_info *kvi, config_fn_t fn, void *data) diff --git a/config.h b/config.h index ba426a960af9f4..26a2850d15afed 100644 --- a/config.h +++ b/config.h @@ -337,6 +337,8 @@ void repo_config_set(struct repository *, const char *, const char *); int git_config_parse_key(const char *, char **, size_t *); +int git_config_key_is_valid(const char *); + /* * The following macros specify flag bits that alter the behavior * of the repo_config_set_multivar*() methods. From 03c29e2e980da7595cbade29e02616d2de2c42f8 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Tue, 2 Jun 2026 18:43:28 +0000 Subject: [PATCH 068/436] config: improve diagnostic for "set" with missing value "git config set pull.rebase=false" currently fails with "wrong number of arguments", and the implicit form "git config pull.rebase=false" fails with "invalid key". Neither points at the real problem: the value is missing. Report that directly, and when the argument has the shape "=", also suggest the split form: $ git config set pull.rebase=false error: missing value to set to the variable 'pull.rebase=false' hint: did you mean "git config set pull.rebase false"? When the prefix before "=" is not a valid key, drop the hint: $ git config set foo=bar error: missing value to set to a variable with an invalid name 'foo=bar' Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/config.c | 32 ++++++++++++++++++++++++++- t/t1300-config.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/builtin/config.c b/builtin/config.c index cf4ba0f7cc6f22..8d8ec0beead220 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -1,6 +1,7 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" #include "abspath.h" +#include "advice.h" #include "config.h" #include "color.h" #include "date.h" @@ -210,6 +211,26 @@ static void check_argc(int argc, int min, int max) exit(129); } +static NORETURN void die_missing_set_value(const char *arg) +{ + const char *last_dot = strrchr(arg, '.'); + const char *eq = last_dot ? strchr(last_dot + 1, '=') : NULL; + char *prefix = eq ? xstrndup(arg, eq - arg) : NULL; + + if (prefix && git_config_key_is_valid(prefix)) { + error(_("missing value to set to the variable '%s'"), arg); + advise(_("did you mean \"git config set %s %s\"?"), + prefix, eq + 1); + } else if (git_config_key_is_valid(arg)) { + error(_("missing value to set to the variable '%s'"), arg); + } else { + error(_("missing value to set to a variable with an invalid name '%s'"), + arg); + } + free(prefix); + exit(129); +} + static void show_config_origin(const struct config_display_options *opts, const struct key_value_info *kvi, struct strbuf *buf) @@ -1133,6 +1154,8 @@ static int cmd_config_set(int argc, const char **argv, const char *prefix, argc = parse_options(argc, argv, prefix, opts, builtin_config_set_usage, PARSE_OPT_STOP_AT_NON_OPTION); + if (argc == 1) + die_missing_set_value(argv[0]); check_argc(argc, 2, 2); if ((flags & CONFIG_FLAGS_FIXED_VALUE) && !value_pattern) @@ -1371,6 +1394,7 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix) }; char *value = NULL, *comment = NULL; int ret = 0; + int actions_implicit; struct key_value_info default_kvi = KVI_INIT; argc = parse_options(argc, argv, prefix, opts, @@ -1385,7 +1409,8 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix) exit(129); } - if (actions == 0) + actions_implicit = (actions == 0); + if (actions_implicit) switch (argc) { case 1: actions = ACTION_GET; break; case 2: actions = ACTION_SET; break; @@ -1394,6 +1419,11 @@ static int cmd_config_actions(int argc, const char **argv, const char *prefix) error(_("no action specified")); exit(129); } + if (actions_implicit && argc == 1) { + const char *last_dot = strrchr(argv[0], '.'); + if (last_dot && strchr(last_dot + 1, '=')) + die_missing_set_value(argv[0]); + } if (display_opts.omit_values && !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) { error(_("--name-only is only applicable to --list or --get-regexp")); diff --git a/t/t1300-config.sh b/t/t1300-config.sh index 128971ee12fa6c..e53c8ecea1304e 100755 --- a/t/t1300-config.sh +++ b/t/t1300-config.sh @@ -462,6 +462,62 @@ test_expect_success 'invalid key' ' test_must_fail git config inval.2key blabla ' +test_expect_success 'set with 1 arg of "key=value": valid key suggests split form' ' + test_must_fail git config set pull.rebase=false 2>err && + test_grep "missing value to set to the variable .pull\\.rebase=false." err && + test_grep "did you mean .git config set pull\\.rebase false." err +' + +test_expect_success 'set with 1 arg of "key=value": implicit form suggests split form' ' + test_must_fail git config pull.rebase=false 2>err && + test_grep "missing value to set to the variable .pull\\.rebase=false." err && + test_grep "did you mean .git config set pull\\.rebase false." err +' + +test_expect_success 'set with 1 arg of "key=value": invalid key does not suggest split form' ' + test_must_fail git config set foo=bar 2>err && + test_grep "missing value to set to a variable with an invalid name .foo=bar." err && + test_grep ! "did you mean" err +' + +test_expect_success 'set with 1 arg: variable name starting with digit is invalid' ' + test_must_fail git config set foo.1bar=baz 2>err && + test_grep "missing value to set to a variable with an invalid name .foo\\.1bar=baz." err && + test_grep ! "did you mean" err +' + +test_expect_success 'set with 1 arg: digit-led section name is valid' ' + test_must_fail git config set 1foo.bar=baz 2>err && + test_grep "missing value to set to the variable .1foo\\.bar=baz." err && + test_grep "did you mean .git config set 1foo\\.bar baz." err +' + +test_expect_success 'set with 1 arg: subsection plus invalid variable name' ' + test_must_fail git config set foo.some.b_r=baz 2>err && + test_grep "missing value to set to a variable with an invalid name .foo\\.some\\.b_r=baz." err && + test_grep ! "did you mean" err +' + +test_expect_success 'set with 1 arg of valid key reports missing value' ' + test_must_fail git config set pull.rebase 2>err && + test_grep "missing value to set to the variable .pull\\.rebase." err && + test_grep ! "did you mean" err +' + +test_expect_success 'set with 2 args including "=" in invalid key does not suggest' ' + test_must_fail git config set pull.rebase=false true 2>err && + test_grep "invalid key: pull\\.rebase=false" err && + test_grep ! "did you mean" err +' + +test_expect_success '"=" inside subsection is valid' ' + test_when_finished "rm -f subsection.cfg" && + git config set -f subsection.cfg foo.bar=baz.boo qux && + echo qux >expect && + git config get -f subsection.cfg foo.bar=baz.boo >actual && + test_cmp expect actual +' + test_expect_success 'correct key' ' git config 123456.a123 987 ' From 9708b3dc95a22116c4a058b107b063da4bcf7d4a Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:31 +0200 Subject: [PATCH 069/436] gitlab-ci: rearrange Linux jobs to match GitHub's order Rearrange the order of Linux jobs that we have defined in GitLab CI so that it matches the order on GitHub's side. This makes it easier to compare whether the list of jobs actually matches on both sides. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .gitlab-ci.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 83ec786c5a49d0..c4eec6e7651300 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,15 +42,15 @@ test:linux: - jobname: linux-reftable image: ubuntu:rolling CC: clang + - jobname: linux-TEST-vars + image: ubuntu:20.04 + CC: gcc + CC_PACKAGE: gcc-8 - jobname: linux-breaking-changes image: ubuntu:20.04 CC: gcc - jobname: fedora-breaking-changes-meson image: fedora:latest - - jobname: linux-TEST-vars - image: ubuntu:20.04 - CC: gcc - CC_PACKAGE: gcc-8 - jobname: linux-leaks image: ubuntu:rolling CC: gcc @@ -60,13 +60,14 @@ test:linux: - jobname: linux-asan-ubsan image: ubuntu:rolling CC: clang + - jobname: linux-meson + image: ubuntu:rolling + CC: gcc - jobname: linux-musl-meson image: alpine:latest + # Supported until 2025-04-02. - jobname: linux32 image: i386/ubuntu:20.04 - - jobname: linux-meson - image: ubuntu:rolling - CC: gcc artifacts: paths: - t/failed-test-artifacts From f0ba41bae89ae5bb66d1b9677d26bd6d7953da34 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:32 +0200 Subject: [PATCH 070/436] gitlab-ci: add missing Linux jobs The GitLab CI definitions are missing jobs for AlmaLinux and Debian, both of which exist in GitHub Workflows. Plug this gap. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .gitlab-ci.yml | 6 ++++++ ci/lib.sh | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c4eec6e7651300..2b9ed44eaf24c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -68,6 +68,12 @@ test:linux: # Supported until 2025-04-02. - jobname: linux32 image: i386/ubuntu:20.04 + # A RHEL 8 compatible distro. Supported until 2029-05-31. + - jobname: almalinux-8 + image: almalinux:8 + # Supported until 2026-08-31. + - jobname: debian-11 + image: debian:11 artifacts: paths: - t/failed-test-artifacts diff --git a/ci/lib.sh b/ci/lib.sh index 6e3799cfc3ccd5..b939110a6eefcf 100755 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -254,7 +254,7 @@ then CI_OS_NAME=osx JOBS=$(nproc) ;; - *,alpine:*|*,fedora:*|*,ubuntu:*|*,i386/ubuntu:*) + *,almalinux:*|*,alpine:*|*,debian:*|*,fedora:*|*,ubuntu:*|*,i386/ubuntu:*) CI_OS_NAME=linux JOBS=$(nproc) ;; From 43a6a005c8970f13c46202e821111f9e42538b9d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:33 +0200 Subject: [PATCH 071/436] ci: unify Linux images across GitLab and GitHub The image for the "linux-breaking-changes" job has drifted apart across GitHub and GitLab. Adapt it to use "ubuntu:rolling" on both systems. With this change there's only one difference remaining: GitHub uses "ubuntu:focal" for the "linux32" job while GitLab uses "ubuntu:20.04". These are different names for the same image, so there is no actual difference here. Adjust GitHub to use the "20.04" tag -- this matches all the other jobs which use version numbers, and you don't have to learn Ubuntu's release names by heart. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- .github/workflows/main.yml | 2 +- .gitlab-ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3da5326f0ba90a..cf341d74dbff21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -407,7 +407,7 @@ jobs: image: alpine:latest # Supported until 2025-04-02. - jobname: linux32 - image: i386/ubuntu:focal + image: i386/ubuntu:20.04 # A RHEL 8 compatible distro. Supported until 2029-05-31. - jobname: almalinux-8 image: almalinux:8 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b9ed44eaf24c4..ef1c723355d153 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -47,7 +47,7 @@ test:linux: CC: gcc CC_PACKAGE: gcc-8 - jobname: linux-breaking-changes - image: ubuntu:20.04 + image: ubuntu:rolling CC: gcc - jobname: fedora-breaking-changes-meson image: fedora:latest From bf3ed750cb4479db9e8193ae1937b2723054ce48 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:34 +0200 Subject: [PATCH 072/436] t7527: fix broken TAP output Before running the tests in t7527 we first verify whether the fsmonitor even works, which seems to depend on the actual filesystem that is in use. The verification executes outside of any prerequisite or test body, so its stdout/stderr is not being redirected. The consequence of this is that any command that prints to stdout/stderr may break the TAP specification by printing invalid lines. And in fact we already do that, as git-init(1) prints the path to the created Git repository by default. Fix this issue by moving the logic into a lazy prerequisite. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7527-builtin-fsmonitor.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/t/t7527-builtin-fsmonitor.sh b/t/t7527-builtin-fsmonitor.sh index b63c162f9bac3f..d881e27466c18e 100755 --- a/t/t7527-builtin-fsmonitor.sh +++ b/t/t7527-builtin-fsmonitor.sh @@ -25,7 +25,8 @@ maybe_timeout () { "$@" fi } -verify_fsmonitor_works () { + +test_lazy_prereq FSMONITOR_WORKS ' git init test_fsmonitor_smoke || return 1 GIT_TRACE_FSMONITOR="$PWD/smoke.trace" && @@ -50,9 +51,9 @@ verify_fsmonitor_works () { ret=$? rm -rf test_fsmonitor_smoke smoke.trace return $ret -} +' -if ! verify_fsmonitor_works +if ! test_have_prereq FSMONITOR_WORKS then skip_all="filesystem does not deliver fsmonitor events (container/overlayfs?)" test_done From b1688db759de18a8403945090688b8cc25ba26dd Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:35 +0200 Subject: [PATCH 073/436] t7810: turn MB_REGEX check into a lazy prereq In t7810 we verify whether the system has proper multibyte locale support by executing `test-tool regex` with a unicode character. When this check fails though we'll output an error that breaks the TAP format. Fix this issue by turning the logic into a lazy prerequisite. Reported-by: Jeff King Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t7810-grep.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index 1b195bee599a37..d61c4a4d73c390 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -18,8 +18,9 @@ test_invalid_grep_expression() { ' } -LC_ALL=en_US.UTF-8 test-tool regex '^.$' '¿' && - test_set_prereq MB_REGEX +test_lazy_prereq MB_REGEX ' + LC_ALL=en_US.UTF-8 test-tool regex "^.$" "¿" +' cat >hello.c < From d11968661e641ea81f4c1938ae9f73a54107dc62 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:36 +0200 Subject: [PATCH 074/436] t/test-lib: silence EBUSY errors on Windows during test cleanup When tests have finished we clean up the trash directory via `rm -rf`. On Windows this can fail with EBUSY in cases where a process still holds some of the files open, for example when we have spawned a daemonized process that wasn't properly terminated. We thus retry several times, but every failure will result in error messages being printed, and that in turn breaks the TAP output format. One such case where this is causing issues is in t921x, which contains tests related to Scalar. Some tests spawn the fsmonitor daemon, and we never properly terminate it. The obvious fix would be to ensure that we never leak any processes, but that gets ugly fast. Instead, let's work around the issue by silencing error messages printed by the `rm -rf` calls. We already know to print an error when the retry loop fails, so we don't loose much. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/test-lib.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/test-lib.sh b/t/test-lib.sh index 4a7357b547e77e..d1d24c4124fd1d 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1299,10 +1299,10 @@ test_done () { error "Tests passed but trash directory already removed before test cleanup; aborting" cd "$TRASH_DIRECTORY/.." && - rm -fr "$TRASH_DIRECTORY" || { + rm -fr "$TRASH_DIRECTORY" 2>/dev/null || { # try again in a bit sleep 5; - rm -fr "$TRASH_DIRECTORY" + rm -fr "$TRASH_DIRECTORY" 2>/dev/null } || error "Tests passed but test cleanup failed; aborting" fi From c2d2d173ae6ba4b354a36b3ba732c8a11379d6ec Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:37 +0200 Subject: [PATCH 075/436] t/lib-git-p4: silence output when killing p4d and its watchdog When stopping the p4d watchdog process via "kill -9", the shell may print a job-control notification like: ./test-lib.sh: line 1269: 57960 Killed: 9 while true; do if test $nr_tries_left -eq 0; then kill -9 $p4d_pid; exit 1; fi; sleep 1; nr_tries_left=$(($nr_tries_left - 1)); done 2> /dev/null 4>&2 (wd: ~) This message is printed asynchronously by the shell when it reaps the process. While harmless right now, this will cause breakage once we enable strict parsing of the TAP protocol in a subsequent commit. Fix this by using `wait` so that we can synchronously reap the watchdog process and swallow the diagnostic. While at it, deduplicate the logic we have in `stop_p4d_and_watchdog ()` and `stop_and_cleanup_p4d ()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/lib-git-p4.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/lib-git-p4.sh b/t/lib-git-p4.sh index d22e9c684a495a..910886818768f0 100644 --- a/t/lib-git-p4.sh +++ b/t/lib-git-p4.sh @@ -65,6 +65,7 @@ pidfile="$TRASH_DIRECTORY/p4d.pid" stop_p4d_and_watchdog () { kill -9 $p4d_pid $watchdog_pid + wait $p4d_pid $watchdog_pid 2>/dev/null } # git p4 submit generates a temp file, which will @@ -174,8 +175,7 @@ retry_until_success () { } stop_and_cleanup_p4d () { - kill -9 $p4d_pid $watchdog_pid - wait $p4d_pid + stop_p4d_and_watchdog rm -rf "$db" "$cli" "$pidfile" } From 389c83025dbde15d30d0791281133bf30e45078d Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 12:07:38 +0200 Subject: [PATCH 076/436] t: let prove fail when parsing invalid TAP output To make the result of our tests accessible we use the TAP protocol. This protocol is parsed by either prove or by Meson. Unfortunately, these two tools differ when it comes to their strictness when parsing the protocol: - Prove by default happily accepts lines not specified by the protocol. - Meson will also accept such lines, but prints a big and ugly warning message. We have fixed our test suite in the past to not print invalid TAP lines anymore via b1dc2e796e (Merge branch 'ps/meson-tap-parse', 2025-06-17). But as none of our tools perform a strict check it's still possible for broken tests to sneak back in, like for example in 362f69547f (Merge branch 'ps/t1006-tap-fix', 2025-07-16). This doesn't hurt at all when using prove, but it's quite annoying when using Meson due to the generated warnings. Unfortunately, there doesn't seem to be a portable way to make all tools complain about violations of the TAP format. The TAP 14 specification has added pragmas to the protocol that would allow us to say `pragma +strict`, and the effect of that would be to treat invalid TAP lines as a test failure. But the release of TAP 14 is still rather recent, and Test-Harness for example only gained support for it in version 3.48, which was released in 2023. In fact though, this pragma was already introduced as an inofficial extension of the TAP protocol with Test-Harness 3.10, released in 2008. So while not all tools understand the pragma, at least prove does for a long time. Unconditionally enable the pragma when using prove so that we'll detect tests that emit broken TAP output right away. This would have detected the issues fixed in preceding commits: $ prove t7527-builtin-fsmonitor.sh t7527-builtin-fsmonitor.sh .. All 69 subtests passed (less 6 skipped subtests: 63 okay) Test Summary Report ------------------- t7527-builtin-fsmonitor.sh (Wstat: 0 Tests: 69 Failed: 0) Parse errors: Unknown TAP token: "Initialized empty Git repository in /tmp/git/test_fsmonitor_smoke/.git/" Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/test-lib.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/t/test-lib.sh b/t/test-lib.sh index d1d24c4124fd1d..ceefb99bff60e0 100644 --- a/t/test-lib.sh +++ b/t/test-lib.sh @@ -1532,6 +1532,12 @@ then BAIL_OUT 'You need to build test-tool; Run "make t/helper/test-tool" in the source (toplevel) directory' fi +if test -n "$HARNESS_ACTIVE" +then + say "TAP version 13" + say "pragma +strict" +fi + # Are we running this test at all? remove_trash= this_test=${0##*/} From 027e3b3d38fa7989b17bdf60501d5f1617141688 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:25 +0200 Subject: [PATCH 077/436] t0001: plug test gaps for git-init(1) with GIT_OBJECT_DIRECTORY In subsequent commits we'll rework how we set up the repository. This is a somewhat intricate and thus fragile sequence; there's many things that can go subtly wrong, and there are lots of interesting interactions that one can discover. One such discovered edge case was the interaction between git-init(1) and the "GIT_OBJECT_DIRECTORY" environment variable. When set, the behaviour is that the object directory should be created at the path that the variable points to. This behaviour is documented as such in its man page: If the object storage directory is specified via the GIT_OBJECT_DIRECTORY environment variable then the sha1 directories are created underneath; otherwise, the default $GIT_DIR/objects directory is used. Curiously enough though we don't seem to have any tests that exercise this directly, and thus a subsequent commit inadvertently would have broken this expectation. Plug this test gap. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- t/t0001-init.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/t/t0001-init.sh b/t/t0001-init.sh index e4d32bb4d259f6..e89feca544d29c 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -980,4 +980,14 @@ test_expect_success 're-init reads matching includeIf.onbranch' ' test_cmp expect err ' +test_expect_success 'init honors GIT_OBJECT_DIRECTORY' ' + test_when_finished "rm -rf init-objdir custom-odb" && + mkdir custom-odb && + env GIT_OBJECT_DIRECTORY="$(pwd)/custom-odb" \ + git init init-objdir && + test_path_is_missing init-objdir/.git/objects/pack && + test_path_is_dir custom-odb/pack && + test_path_is_dir custom-odb/info +' + test_done From 452ad8db6d9155d6c7305d6045d29c49a7cc9c7c Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:26 +0200 Subject: [PATCH 078/436] setup: drop `setup_git_env()` The `setup_git_env()` function is a trivial wrapper around `setup_git_env_internal()` and has a single call site only. Drop the function. While at it, drop stale documentation in "environment.h" that points to this function, even though it hasn't been exposed to callers outside of "setup.c" since 43ad1047a9 (setup: stop using `the_repository` in `setup_git_env()`, 2026-03-27) anymore. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- environment.h | 8 +------- refs.c | 3 ++- setup.c | 7 +------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/environment.h b/environment.h index 9eb97b3869c9b1..ccfcf37bfb9b99 100644 --- a/environment.h +++ b/environment.h @@ -130,13 +130,6 @@ void repo_config_values_init(struct repo_config_values *cfg); * `the_repository`. We should eventually get rid of these and make the * dependency on a repository explicit: * - * - `setup_git_env()` ideally shouldn't exist as it modifies global state, - * namely the environment. The current process shouldn't ever access that - * state via envvars though, but should instead consult a `struct - * repository`. When spawning new processes, we would ideally also pass a - * `struct repository` and then set up the environment variables for the - * child process, only. - * * - `have_git_dir()` should not have to exist at all. Instead, we should * decide on whether or not we have a `struct repository`. * @@ -147,6 +140,7 @@ void repo_config_values_init(struct repo_config_values *cfg); * Please do not add new global config variables here. */ # ifdef USE_THE_REPOSITORY_VARIABLE + /* * Returns true iff we have a configured git repository (either via * setup_git_directory, or in the environment via $GIT_DIR). diff --git a/refs.c b/refs.c index 0f3355d2ee0be1..e7070eb7432db0 100644 --- a/refs.c +++ b/refs.c @@ -126,7 +126,8 @@ struct ref_namespace_info ref_namespace[] = { * points to the content of another. Unlike the other * ref namespaces, this one can be changed by the * GIT_REPLACE_REF_BASE environment variable. This - * .namespace value will be overwritten in setup_git_env(). + * .namespace value will be overwritten during repository + * setup. */ .ref = "refs/replace/", .decoration = DECORATION_GRAFTED, diff --git a/setup.c b/setup.c index d723306dfe5256..252b4431172265 100644 --- a/setup.c +++ b/setup.c @@ -1074,11 +1074,6 @@ static void setup_git_env_internal(struct repository *repo, fetch_if_missing = 0; } -static void setup_git_env(struct repository *repo, const char *git_dir) -{ - setup_git_env_internal(repo, git_dir, false); -} - static void set_git_dir_1(struct repository *repo, const char *path, bool skip_initializing_odb) { xsetenv(GIT_DIR_ENVIRONMENT, path, 1); @@ -2023,7 +2018,7 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); if (!gitdir) gitdir = DEFAULT_GIT_DIR_ENVIRONMENT; - setup_git_env(repo, gitdir); + setup_git_env_internal(repo, gitdir, false); } if (startup_info->have_repository) { repo_set_hash_algo(repo, repo_fmt.hash_algo); From 3d884b0b5656fe012002edd6bb8f36a125e6c17e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:27 +0200 Subject: [PATCH 079/436] setup: deduplicate logic to apply repository format After having discovered the repository format we then apply it to the repository so that it knows to use the proper repository extensions. The logic to apply the format is duplicated across three callsites, which makes it rather painfull to add new extensions. Introduce a new function `apply_repository_format()` that takes a repo and applies a given format to it and adapt all callsites to use it. This function is also the new caller of `verify_repository_format()` so that we can ensure that we never apply an invalid repository format. The verification we have in `read_and_verify_repository_format()` is thus redundant now and dropped. Rename `read_and_verify_repository_format()` accordingly. While at it, also rename `check_repository_format()` to clarify that it doesn't only _check_ the format, but that it also applies it. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- repository.c | 31 +++++++----------- setup.c | 93 ++++++++++++++++++++++++++++------------------------ setup.h | 10 ++++++ 3 files changed, 71 insertions(+), 63 deletions(-) diff --git a/repository.c b/repository.c index db57b8308b94e7..58a13f7c4f5d85 100644 --- a/repository.c +++ b/repository.c @@ -262,8 +262,8 @@ void repo_set_worktree(struct repository *repo, const char *path) trace2_def_repo(repo); } -static int read_and_verify_repository_format(struct repository_format *format, - const char *commondir) +static int read_repository_format_from_commondir(struct repository_format *format, + const char *commondir) { int ret = 0; struct strbuf sb = STRBUF_INIT; @@ -272,11 +272,6 @@ static int read_and_verify_repository_format(struct repository_format *format, read_repository_format(format, sb.buf); strbuf_reset(&sb); - if (verify_repository_format(format, &sb) < 0) { - warning("%s", sb.buf); - ret = -1; - } - strbuf_release(&sb); return ret; } @@ -290,6 +285,8 @@ int repo_init(struct repository *repo, const char *worktree) { struct repository_format format = REPOSITORY_FORMAT_INIT; + struct strbuf err = STRBUF_INIT; + memset(repo, 0, sizeof(*repo)); initialize_repository(repo); @@ -297,21 +294,13 @@ int repo_init(struct repository *repo, if (repo_init_gitdir(repo, gitdir)) goto error; - if (read_and_verify_repository_format(&format, repo->commondir)) + if (read_repository_format_from_commondir(&format, repo->commondir)) goto error; - repo_set_hash_algo(repo, format.hash_algo); - repo_set_compat_hash_algo(repo, format.compat_hash_algo); - repo_set_ref_storage_format(repo, format.ref_storage_format, - format.ref_storage_payload); - repo->repository_format_worktree_config = format.worktree_config; - repo->repository_format_relative_worktrees = format.relative_worktrees; - repo->repository_format_precious_objects = format.precious_objects; - repo->repository_format_submodule_path_cfg = format.submodule_path_cfg; - - /* take ownership of format.partial_clone */ - repo->repository_format_partial_clone = format.partial_clone; - format.partial_clone = NULL; + if (apply_repository_format(repo, &format, &err) < 0) { + warning("%s", err.buf); + goto error; + } if (worktree) repo_set_worktree(repo, worktree); @@ -320,10 +309,12 @@ int repo_init(struct repository *repo, repo_read_loose_object_map(repo); clear_repository_format(&format); + strbuf_release(&err); return 0; error: clear_repository_format(&format); + strbuf_release(&err); repo_clear(repo); return -1; } diff --git a/setup.c b/setup.c index 252b4431172265..c5015923f159a9 100644 --- a/setup.c +++ b/setup.c @@ -750,8 +750,7 @@ static int check_repo_format(const char *var, const char *value, return read_worktree_config(var, value, ctx, vdata); } -static int check_repository_format_gently(struct repository *repo, - const char *gitdir, +static int check_repository_format_gently(const char *gitdir, struct repository_format *candidate, int *nongit_ok) { @@ -765,7 +764,7 @@ static int check_repository_format_gently(struct repository *repo, strbuf_release(&sb); /* - * For historical use of check_repository_format() in git-init, + * For historical use of check_and_apply_repository_format() in git-init, * we treat a missing config as a silent "ok", even when nongit_ok * is unset. */ @@ -782,8 +781,6 @@ static int check_repository_format_gently(struct repository *repo, die("%s", err.buf); } - repo->repository_format_precious_objects = candidate->precious_objects; - string_list_clear(&candidate->unknown_extensions, 0); string_list_clear(&candidate->v1_only_extensions, 0); @@ -1140,7 +1137,7 @@ static const char *setup_explicit_git_dir(struct repository *repo, die(_("not a git repository: '%s'"), gitdirenv); } - if (check_repository_format_gently(repo, gitdirenv, repo_fmt, nongit_ok)) { + if (check_repository_format_gently(gitdirenv, repo_fmt, nongit_ok)) { free(gitfile); return NULL; } @@ -1217,7 +1214,7 @@ static const char *setup_discovered_git_dir(struct repository *repo, struct repository_format *repo_fmt, int *nongit_ok) { - if (check_repository_format_gently(repo, gitdir, repo_fmt, nongit_ok)) + if (check_repository_format_gently(gitdir, repo_fmt, nongit_ok)) return NULL; /* --work-tree is set without --git-dir; use discovered one */ @@ -1265,7 +1262,7 @@ static const char *setup_bare_git_dir(struct repository *repo, { int root_len; - if (check_repository_format_gently(repo, ".", repo_fmt, nongit_ok)) + if (check_repository_format_gently(".", repo_fmt, nongit_ok)) return NULL; setenv(GIT_IMPLICIT_WORK_TREE_ENVIRONMENT, "0", 1); @@ -1757,6 +1754,32 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir, return result; } +int apply_repository_format(struct repository *repo, + const struct repository_format *format, + struct strbuf *err) +{ + if (verify_repository_format(format, err) < 0) + return -1; + + repo_set_hash_algo(repo, format->hash_algo); + repo_set_compat_hash_algo(repo, format->compat_hash_algo); + repo_set_ref_storage_format(repo, + format->ref_storage_format, + format->ref_storage_payload); + repo->repository_format_worktree_config = + format->worktree_config; + repo->repository_format_submodule_path_cfg = + format->submodule_path_cfg; + repo->repository_format_relative_worktrees = + format->relative_worktrees; + repo->repository_format_partial_clone = + xstrdup_or_null(format->partial_clone); + repo->repository_format_precious_objects = + format->precious_objects; + + return 0; +} + /* * Check the repository format version in the path found in repo_get_git_dir(repo), * and die if it is a version we don't understand. Generally one would @@ -1765,26 +1788,20 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir, * * If successful and fmt is not NULL, fill fmt with data. */ -static void check_repository_format(struct repository *repo, struct repository_format *fmt) +static void check_and_apply_repository_format(struct repository *repo, + struct repository_format *fmt) { struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; + struct strbuf err = STRBUF_INIT; + if (!fmt) fmt = &repo_fmt; - check_repository_format_gently(repo, repo_get_git_dir(repo), fmt, NULL); + + check_repository_format_gently(repo_get_git_dir(repo), fmt, NULL); + if (apply_repository_format(repo, fmt, &err) < 0) + die("%s", err.buf); startup_info->have_repository = 1; - repo_set_hash_algo(repo, fmt->hash_algo); - repo_set_compat_hash_algo(repo, fmt->compat_hash_algo); - repo_set_ref_storage_format(repo, - fmt->ref_storage_format, - fmt->ref_storage_payload); - repo->repository_format_worktree_config = - fmt->worktree_config; - repo->repository_format_submodule_path_cfg = - fmt->submodule_path_cfg; - repo->repository_format_relative_worktrees = - fmt->relative_worktrees; - repo->repository_format_partial_clone = - xstrdup_or_null(fmt->partial_clone); + clear_repository_format(&repo_fmt); } @@ -1862,7 +1879,7 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags if (is_git_directory(".")) { set_git_dir(repo, ".", 0); - check_repository_format(repo, NULL); + check_and_apply_repository_format(repo, NULL); return path; } @@ -2020,25 +2037,15 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) gitdir = DEFAULT_GIT_DIR_ENVIRONMENT; setup_git_env_internal(repo, gitdir, false); } + if (startup_info->have_repository) { - repo_set_hash_algo(repo, repo_fmt.hash_algo); - repo_set_compat_hash_algo(repo, - repo_fmt.compat_hash_algo); - repo_set_ref_storage_format(repo, - repo_fmt.ref_storage_format, - repo_fmt.ref_storage_payload); - repo->repository_format_worktree_config = - repo_fmt.worktree_config; - repo->repository_format_relative_worktrees = - repo_fmt.relative_worktrees; - repo->repository_format_submodule_path_cfg = - repo_fmt.submodule_path_cfg; - /* take ownership of repo_fmt.partial_clone */ - repo->repository_format_partial_clone = - repo_fmt.partial_clone; - repo_fmt.partial_clone = NULL; - repo->repository_format_precious_objects = - repo_fmt.precious_objects; + struct strbuf err = STRBUF_INIT; + + if (apply_repository_format(repo, &repo_fmt, &err) < 0) + die("%s", err.buf); + + clear_repository_format(&repo_fmt); + strbuf_release(&err); } } /* @@ -2814,7 +2821,7 @@ int init_db(struct repository *repo, * config file, so this will not fail. What we are catching * is an attempt to reinitialize new repository with an old tool. */ - check_repository_format(repo, &repo_fmt); + check_and_apply_repository_format(repo, &repo_fmt); repository_format_configure(repo, &repo_fmt, hash, ref_storage_format); diff --git a/setup.h b/setup.h index 9409326fe47c70..efbb82fdbfc80b 100644 --- a/setup.h +++ b/setup.h @@ -221,6 +221,16 @@ void clear_repository_format(struct repository_format *format); int verify_repository_format(const struct repository_format *format, struct strbuf *err); +/* + * Apply the given repository format to the repo. This initializes extensions + * and basic data structures required for normal operation. Returns 0 on + * success, a negative error code when the format is not valid as determined by + * `verify_repository_format()`. + */ +int apply_repository_format(struct repository *repo, + const struct repository_format *format, + struct strbuf *err); + const char *get_template_dir(const char *option_template); #define INIT_DB_QUIET (1 << 0) From 6a2fbab4c95b0fc317514ec7ead618b3b37e3553 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:28 +0200 Subject: [PATCH 080/436] repository: stop initializing the object database in `repo_set_gitdir()` The function `repo_set_gitdir()` obviously sets the Git directory for a given repository. Less obviously though, the function also configures a couple of auxiliary settings. One such thing is that we create the object database in this function. This logic only happens conditionally though, as `set_git_dir()` may be called multiple times during repository setup, and we don't want to create the object database multiple times. This is somewhat tangled and hard to follow. Remove the logic from `repo_set_gitdir()` and instead initialize the object database outside of it. This leads to some duplication right now, but that duplication will be removed in a subsequent step where we will start initializing the object database as part of applying the repo's format. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- repository.c | 8 ++------ repository.h | 3 --- setup.c | 7 ++++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/repository.c b/repository.c index 58a13f7c4f5d85..2c2395105fcf2d 100644 --- a/repository.c +++ b/repository.c @@ -181,12 +181,6 @@ void repo_set_gitdir(struct repository *repo, free(old_gitdir); repo_set_commondir(repo, o->commondir); - - if (!repo->objects) - repo->objects = odb_new(repo, o->object_dir, o->alternate_db); - else if (!o->skip_initializing_odb) - BUG("cannot reinitialize an already-initialized object directory"); - repo->disable_ref_updates = o->disable_ref_updates; expand_base_dir(&repo->graft_file, o->graft_file, @@ -302,6 +296,8 @@ int repo_init(struct repository *repo, goto error; } + repo->objects = odb_new(repo, NULL, NULL); + if (worktree) repo_set_worktree(repo, worktree); diff --git a/repository.h b/repository.h index c3ec0f4b790b00..36e2db26332c0e 100644 --- a/repository.h +++ b/repository.h @@ -221,12 +221,9 @@ const char *repo_get_work_tree(struct repository *repo); */ struct set_gitdir_args { const char *commondir; - const char *object_dir; const char *graft_file; const char *index_file; - const char *alternate_db; bool disable_ref_updates; - bool skip_initializing_odb; }; void repo_set_gitdir(struct repository *repo, const char *root, diff --git a/setup.c b/setup.c index c5015923f159a9..3bd3f6c5924ef3 100644 --- a/setup.c +++ b/setup.c @@ -1045,17 +1045,18 @@ static void setup_git_env_internal(struct repository *repo, struct strvec to_free = STRVEC_INIT; args.commondir = getenv_safe(&to_free, GIT_COMMON_DIR_ENVIRONMENT); - args.object_dir = getenv_safe(&to_free, DB_ENVIRONMENT); args.graft_file = getenv_safe(&to_free, GRAFT_ENVIRONMENT); args.index_file = getenv_safe(&to_free, INDEX_ENVIRONMENT); - args.alternate_db = getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT); if (getenv(GIT_QUARANTINE_ENVIRONMENT)) args.disable_ref_updates = true; - args.skip_initializing_odb = skip_initializing_odb; repo_set_gitdir(repo, git_dir, &args); strvec_clear(&to_free); + if (!skip_initializing_odb) + repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT), + getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); + if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT)) disable_replace_refs(); replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT); From aae4ebc895272dc7e5a9ccfc135878b55c7322d7 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:29 +0200 Subject: [PATCH 081/436] setup: stop creating the object database in `setup_git_env()` In the preceding commit we have stopped creating the object database in `repo_set_gitdir()`. But the logic is still somewhat confusing as we still end up creating it conditionally in `setup_git_dir()`, which is called multiple times. Drop the conditional logic and instead create the object database in all places where we have discovered and configured a repository. This leads to even more duplication than we already had in the preceding commit, but an alert reader may notice that we now (almost) always call `odb_new()` directly before having called `apply_repository_format()`. The only exception to this is `setup_git_directory_gently()`, where we also call the function when _not_ applying the repository format. This will be fixed in the next commit, and once that's done we can then unify creation of the object database into `apply_repository_format()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- setup.c | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/setup.c b/setup.c index 3bd3f6c5924ef3..0dc9fe4565a182 100644 --- a/setup.c +++ b/setup.c @@ -1035,8 +1035,7 @@ const char *read_gitfile_gently(const char *path, int *return_error_code) } static void setup_git_env_internal(struct repository *repo, - const char *git_dir, - bool skip_initializing_odb) + const char *git_dir) { char *git_replace_ref_base; const char *shallow_file; @@ -1053,10 +1052,6 @@ static void setup_git_env_internal(struct repository *repo, repo_set_gitdir(repo, git_dir, &args); strvec_clear(&to_free); - if (!skip_initializing_odb) - repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT), - getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); - if (getenv(NO_REPLACE_OBJECTS_ENVIRONMENT)) disable_replace_refs(); replace_ref_base = getenv(GIT_REPLACE_REF_BASE_ENVIRONMENT); @@ -1072,10 +1067,10 @@ static void setup_git_env_internal(struct repository *repo, fetch_if_missing = 0; } -static void set_git_dir_1(struct repository *repo, const char *path, bool skip_initializing_odb) +static void set_git_dir_1(struct repository *repo, const char *path) { xsetenv(GIT_DIR_ENVIRONMENT, path, 1); - setup_git_env_internal(repo, path, skip_initializing_odb); + setup_git_env_internal(repo, path); } static void update_relative_gitdir(const char *name UNUSED, @@ -1089,7 +1084,7 @@ static void update_relative_gitdir(const char *name UNUSED, trace_printf_key(&trace_setup_key, "setup: move $GIT_DIR to '%s'", path); - set_git_dir_1(repo, path, true); + set_git_dir_1(repo, path); free(path); } @@ -1102,7 +1097,7 @@ static void set_git_dir(struct repository *repo, const char *path, int make_real path = realpath.buf; } - set_git_dir_1(repo, path, false); + set_git_dir_1(repo, path); if (!is_absolute_path(path)) chdir_notify_register(NULL, update_relative_gitdir, repo); @@ -1879,8 +1874,15 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags } if (is_git_directory(".")) { + struct strvec to_free = STRVEC_INIT; + set_git_dir(repo, ".", 0); + repo->objects = odb_new(repo, + getenv_safe(&to_free, DB_ENVIRONMENT), + getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); check_and_apply_repository_format(repo, NULL); + + strvec_clear(&to_free); return path; } @@ -2032,13 +2034,19 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) startup_info->have_repository || /* GIT_DIR_EXPLICIT */ getenv(GIT_DIR_ENVIRONMENT)) { + struct strvec to_free = STRVEC_INIT; + if (!repo->gitdir) { const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); if (!gitdir) gitdir = DEFAULT_GIT_DIR_ENVIRONMENT; - setup_git_env_internal(repo, gitdir, false); + setup_git_env_internal(repo, gitdir); } + repo->objects = odb_new(repo, + getenv_safe(&to_free, DB_ENVIRONMENT), + getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); + if (startup_info->have_repository) { struct strbuf err = STRBUF_INIT; @@ -2048,6 +2056,8 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) clear_repository_format(&repo_fmt); strbuf_release(&err); } + + strvec_clear(&to_free); } /* * Since precompose_string_if_needed() needs to look at @@ -2796,6 +2806,7 @@ int init_db(struct repository *repo, int exist_ok = flags & INIT_DB_EXIST_OK; char *original_git_dir = real_pathdup(git_dir, 1); struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; + struct strvec to_free = STRVEC_INIT; if (real_git_dir) { struct stat st; @@ -2816,6 +2827,9 @@ int init_db(struct repository *repo, } startup_info->have_repository = 1; + repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT), + getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); + /* * Check to see if the repository version is right. * Note that a newly created repository does not have @@ -2879,6 +2893,7 @@ int init_db(struct repository *repo, } clear_repository_format(&repo_fmt); + strvec_clear(&to_free); free(original_git_dir); return 0; } From d87de311ff506599ec130ba5f09a4f73e458a5ae Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:30 +0200 Subject: [PATCH 082/436] setup: stop initializing object database without repository The function `setup_git_directory_gently()` is responsible for discovering and setting up a Git repository based on various environment variables and the current working directory. The result is thus a fully usable Git repository. One oddity of this function is that we may set up the object database even in the case where we don't have a repository, namely in the case where the `GIT_DIR_EXPLICIT` environment variable is set but points to a non-existent repository. If so, we call `setup_git_env_internal()` with the value of the environment variable so that the repository's Git directory is configured, even if it points to a non-existent directory. Historically though, this function didn't only configure the repository, but also initialized the object database. We retained this behaviour from a preceding commit, even though it really doesn't make much sense in the first place -- there is no repository, so we don't have an object database either. There seemingly isn't much of a reason to construct the object database, as we typically won't try to read objects when we don't have an object database. There's one exception though: git-index-pack(1) may run outside of a repository, which can be used to perform consistency checks for a packfile. The code path is _almost_ working: we already know to call `parse_object_buffer()`, which can read objects without an object database being available. And that works for all object types except for commits, because `parse_commit_buffer()` calls `parse_commit_graph()`, and that function doesn't handle the case where we don't have an object database. Fix this instance to check for the object database instead of checking for the Git directory having been initialized. With this fixed, we can now stop constructing an object database completely. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- commit-graph.c | 4 ++-- setup.c | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/commit-graph.c b/commit-graph.c index 9abe62bd5a278a..0820cf5fb83cbe 100644 --- a/commit-graph.c +++ b/commit-graph.c @@ -740,13 +740,13 @@ static struct commit_graph *prepare_commit_graph(struct repository *r) struct odb_source *source; /* - * Early return if there is no git dir or if the commit graph is + * Early return if there is no object database or if the commit graph is * disabled. * * This must come before the "already attempted?" check below, because * we want to disable even an already-loaded graph file. */ - if (!r->gitdir || r->commit_graph_disabled) + if (!r->objects || r->commit_graph_disabled) return NULL; if (r->objects->commit_graph_attempted) diff --git a/setup.c b/setup.c index 0dc9fe4565a182..4a8d6230b18529 100644 --- a/setup.c +++ b/setup.c @@ -2043,13 +2043,12 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) setup_git_env_internal(repo, gitdir); } - repo->objects = odb_new(repo, - getenv_safe(&to_free, DB_ENVIRONMENT), - getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); - if (startup_info->have_repository) { struct strbuf err = STRBUF_INIT; + repo->objects = odb_new(repo, + getenv_safe(&to_free, DB_ENVIRONMENT), + getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); if (apply_repository_format(repo, &repo_fmt, &err) < 0) die("%s", err.buf); From a84a9d4acdae51f58529b2596c4bd935fe9af372 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:31 +0200 Subject: [PATCH 083/436] repository: stop reading loose object map twice on repo init When initializing a repository via `repo_init()` we end up reading the loose object map twice: - `apply_repository_format()` calls `repo_set_compat_hash_algo()`, which in turn calls `repo_read_loose_object_map()` if we have a compatibility hash configured. - `repo_init()` calls `repo_read_loose_object_map()` directly a second time. Drop the second read of the loose object map in `repo_init()`. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- repository.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/repository.c b/repository.c index 2c2395105fcf2d..61dfbb8be6cd1d 100644 --- a/repository.c +++ b/repository.c @@ -301,9 +301,6 @@ int repo_init(struct repository *repo, if (worktree) repo_set_worktree(repo, worktree); - if (repo->compat_hash_algo) - repo_read_loose_object_map(repo); - clear_repository_format(&format); strbuf_release(&err); return 0; From 42b9d3dc9dfa9e733cbd6402e665ac35fce0c216 Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Thu, 4 Jun 2026 09:46:32 +0200 Subject: [PATCH 084/436] setup: construct object database in `apply_repository_format()` With the preceding changes we now always construct the repository's object database before applying the repository format. Remove this duplication by constructing it in `apply_repository_format()` instead. Note that we create the object database _after_ having set up the repository's hash algorithm, but _before_ setting the compat hash algorithm. This is intentional: - Constructing the object database may require knowledge of its intended object format. - Setting up the compatibility hash requires the object database to be initialized already, because we immediately read the loose object map. The first point is sensible, the second maybe a little less so. Ideally, it should be the responsibility of the object database itself to initialize any data structures required for the compatibility hash. But this would require further changes, so this is kept as-is for now. Further note that this requires us to move handling of the environment variables GIT_OBJECT_DIRECTORY and GIT_ALTERNATE_OBJECT_DIRECTORIES into the repository format, as well. This allows the caller more flexibility around whether or not those environment variables are being honored, as we want to respect them in "setup.c", but not in "repository.c". Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- repository.c | 4 +--- setup.c | 45 +++++++++++++++++++++------------------------ setup.h | 10 ++++++++++ 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/repository.c b/repository.c index 61dfbb8be6cd1d..187dd471c4e607 100644 --- a/repository.c +++ b/repository.c @@ -291,13 +291,11 @@ int repo_init(struct repository *repo, if (read_repository_format_from_commondir(&format, repo->commondir)) goto error; - if (apply_repository_format(repo, &format, &err) < 0) { + if (apply_repository_format(repo, &format, 0, &err) < 0) { warning("%s", err.buf); goto error; } - repo->objects = odb_new(repo, NULL, NULL); - if (worktree) repo_set_worktree(repo, worktree); diff --git a/setup.c b/setup.c index 4a8d6230b18529..513fc88749212b 100644 --- a/setup.c +++ b/setup.c @@ -1752,12 +1752,22 @@ enum discovery_result discover_git_directory_reason(struct strbuf *commondir, int apply_repository_format(struct repository *repo, const struct repository_format *format, + enum apply_repository_format_flags flags, struct strbuf *err) { + char *object_directory = NULL, *alternate_object_directories = NULL; + if (verify_repository_format(format, err) < 0) return -1; + if (flags & APPLY_REPOSITORY_FORMAT_HONOR_ENV) { + object_directory = xstrdup_or_null(getenv(DB_ENVIRONMENT)); + alternate_object_directories = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT)); + } + repo_set_hash_algo(repo, format->hash_algo); + repo->objects = odb_new(repo, object_directory, + alternate_object_directories); repo_set_compat_hash_algo(repo, format->compat_hash_algo); repo_set_ref_storage_format(repo, format->ref_storage_format, @@ -1773,6 +1783,8 @@ int apply_repository_format(struct repository *repo, repo->repository_format_precious_objects = format->precious_objects; + free(alternate_object_directories); + free(object_directory); return 0; } @@ -1785,7 +1797,8 @@ int apply_repository_format(struct repository *repo, * If successful and fmt is not NULL, fill fmt with data. */ static void check_and_apply_repository_format(struct repository *repo, - struct repository_format *fmt) + struct repository_format *fmt, + enum apply_repository_format_flags flags) { struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; struct strbuf err = STRBUF_INIT; @@ -1794,7 +1807,7 @@ static void check_and_apply_repository_format(struct repository *repo, fmt = &repo_fmt; check_repository_format_gently(repo_get_git_dir(repo), fmt, NULL); - if (apply_repository_format(repo, fmt, &err) < 0) + if (apply_repository_format(repo, fmt, flags, &err) < 0) die("%s", err.buf); startup_info->have_repository = 1; @@ -1874,15 +1887,9 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags } if (is_git_directory(".")) { - struct strvec to_free = STRVEC_INIT; - set_git_dir(repo, ".", 0); - repo->objects = odb_new(repo, - getenv_safe(&to_free, DB_ENVIRONMENT), - getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); - check_and_apply_repository_format(repo, NULL); - - strvec_clear(&to_free); + check_and_apply_repository_format(repo, NULL, + APPLY_REPOSITORY_FORMAT_HONOR_ENV); return path; } @@ -2034,8 +2041,6 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) startup_info->have_repository || /* GIT_DIR_EXPLICIT */ getenv(GIT_DIR_ENVIRONMENT)) { - struct strvec to_free = STRVEC_INIT; - if (!repo->gitdir) { const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); if (!gitdir) @@ -2046,17 +2051,13 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) if (startup_info->have_repository) { struct strbuf err = STRBUF_INIT; - repo->objects = odb_new(repo, - getenv_safe(&to_free, DB_ENVIRONMENT), - getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); - if (apply_repository_format(repo, &repo_fmt, &err) < 0) + if (apply_repository_format(repo, &repo_fmt, + APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); clear_repository_format(&repo_fmt); strbuf_release(&err); } - - strvec_clear(&to_free); } /* * Since precompose_string_if_needed() needs to look at @@ -2805,7 +2806,6 @@ int init_db(struct repository *repo, int exist_ok = flags & INIT_DB_EXIST_OK; char *original_git_dir = real_pathdup(git_dir, 1); struct repository_format repo_fmt = REPOSITORY_FORMAT_INIT; - struct strvec to_free = STRVEC_INIT; if (real_git_dir) { struct stat st; @@ -2826,16 +2826,14 @@ int init_db(struct repository *repo, } startup_info->have_repository = 1; - repo->objects = odb_new(repo, getenv_safe(&to_free, DB_ENVIRONMENT), - getenv_safe(&to_free, ALTERNATE_DB_ENVIRONMENT)); - /* * Check to see if the repository version is right. * Note that a newly created repository does not have * config file, so this will not fail. What we are catching * is an attempt to reinitialize new repository with an old tool. */ - check_and_apply_repository_format(repo, &repo_fmt); + check_and_apply_repository_format(repo, &repo_fmt, + APPLY_REPOSITORY_FORMAT_HONOR_ENV); repository_format_configure(repo, &repo_fmt, hash, ref_storage_format); @@ -2892,7 +2890,6 @@ int init_db(struct repository *repo, } clear_repository_format(&repo_fmt); - strvec_clear(&to_free); free(original_git_dir); return 0; } diff --git a/setup.h b/setup.h index efbb82fdbfc80b..19679fe78fb72f 100644 --- a/setup.h +++ b/setup.h @@ -221,6 +221,15 @@ void clear_repository_format(struct repository_format *format); int verify_repository_format(const struct repository_format *format, struct strbuf *err); +enum apply_repository_format_flags { + /* + * Honor environment variables when applying the repository format to + * the repository. For now, this only covers environment variables that + * relate to the object database. + */ + APPLY_REPOSITORY_FORMAT_HONOR_ENV = (1 << 0), +}; + /* * Apply the given repository format to the repo. This initializes extensions * and basic data structures required for normal operation. Returns 0 on @@ -229,6 +238,7 @@ int verify_repository_format(const struct repository_format *format, */ int apply_repository_format(struct repository *repo, const struct repository_format *format, + enum apply_repository_format_flags flags, struct strbuf *err); const char *get_template_dir(const char *option_template); From e6145d12413044f90ee31eb7d83ae209aeb0adff Mon Sep 17 00:00:00 2001 From: Tuomas Ahola Date: Thu, 4 Jun 2026 16:14:57 +0300 Subject: [PATCH 085/436] docs: fix typos Fix some typos and grammar errors in comments and documentation files. Signed-off-by: Tuomas Ahola Signed-off-by: Junio C Hamano --- Documentation/config/sideband.adoc | 2 +- Documentation/git-format-rev.adoc | 2 +- date.c | 2 +- replay.h | 2 +- t/t9902-completion.sh | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Documentation/config/sideband.adoc b/Documentation/config/sideband.adoc index 96fade7f5fee39..ff007aeb738a3d 100644 --- a/Documentation/config/sideband.adoc +++ b/Documentation/config/sideband.adoc @@ -13,7 +13,7 @@ sideband.allowControlCharacters:: Allow control sequences that move the cursor. This is disabled by default. `erase`:: - Allow control sequences that erase charactrs. This is + Allow control sequences that erase characters. This is disabled by default. `false`:: Mask all control characters other than line feeds and diff --git a/Documentation/git-format-rev.adoc b/Documentation/git-format-rev.adoc index c40d52e9f6d108..505a52feccd466 100644 --- a/Documentation/git-format-rev.adoc +++ b/Documentation/git-format-rev.adoc @@ -33,7 +33,7 @@ OPTIONS The argument `rev` is also accepted. `text`;; Formats all commit object names found in freeform text. These - must the full object names, i.e. abbreviated hexidecimal object + must be full object names, i.e. abbreviated hexadecimal object names will not be interpreted. + Anything that is parsed as an object name but that is not found to be a diff --git a/date.c b/date.c index 05b78d852f0705..014065b419aee7 100644 --- a/date.c +++ b/date.c @@ -1074,7 +1074,7 @@ void datestamp(struct strbuf *out) * * The tm->tm_mday field has an additional logic of using negative values * for date adjustments: -2 means yesterday and -3 the day before that, - * and so on. The idea is to deref such adjustments until we are sure + * and so on. The idea is to defer such adjustments until we are sure * there's no explicit mday specification in the approxidate string. */ static time_t update_tm(struct tm *tm, struct tm *now, time_t sec) diff --git a/replay.h b/replay.h index 1851a07705ab03..faf95c7459e594 100644 --- a/replay.h +++ b/replay.h @@ -32,7 +32,7 @@ struct replay_revisions_options { /* * Starting point at which to create the new commits; must be a - * committish. References pointing at decendants of `onto` will be + * committish. References pointing at descendants of `onto` will be * updated to point to the new commits. */ const char *onto; diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh index 28f61f08fb4cec..55dc9eabfc42fe 100755 --- a/t/t9902-completion.sh +++ b/t/t9902-completion.sh @@ -2444,7 +2444,7 @@ test_expect_success FUNNYNAMES \ >repeated-quoted/2-file && >repeated-quoted/3\"file && # ... and here, too. - # Still, we shold only list the directory name only once. + # Still, we should list the directory name only once. test_path_completion repeated repeated-quoted ' From 179f122aea7074a21ea095337c96801695ba3729 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Jun 2026 16:24:19 +0000 Subject: [PATCH 086/436] mingw: kill child processes in a gentler way The TerminateProcess() function does not actually leave the child processes any chance to perform any cleanup operations. This is bad insofar as Git itself expects its signal handlers to run. A symptom is e.g. a left-behind .lock file that would not be left behind if the same operation was run, say, on Linux. To remedy this situation, we use an obscure trick: we inject a thread into the process that needs to be killed and to let that thread run the ExitProcess() function with the desired exit status. Thanks J Wyman for describing this trick. The advantage is that the ExitProcess() function lets the atexit handlers run. While this is still different from what Git expects (i.e. running a signal handler), in practice Git sets up signal handlers and atexit handlers that call the same code to clean up after itself. In case that the gentle method to terminate the process failed, we still fall back to calling TerminateProcess(), but in that case we now also make sure that processes spawned by the spawned process are terminated; TerminateProcess() does not give the spawned process a chance to do so itself. Please note that this change only affects how Git for Windows tries to terminate processes spawned by Git's own executables. Third-party software that *calls* Git and wants to terminate it *still* need to make sure to imitate this gentle method, otherwise this patch will not have any effect. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 29 +++++-- compat/win32/exit-process.h | 165 ++++++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 compat/win32/exit-process.h diff --git a/compat/mingw.c b/compat/mingw.c index aa7525f419cb64..44c63059cd6916 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -13,6 +13,7 @@ #include "symlinks.h" #include "trace2.h" #include "win32.h" +#include "win32/exit-process.h" #include "win32/lazyload.h" #include "wrapper.h" #include @@ -2251,16 +2252,28 @@ int mingw_execvp(const char *cmd, char *const *argv) int mingw_kill(pid_t pid, int sig) { if (pid > 0 && sig == SIGTERM) { - HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); - - if (TerminateProcess(h, -1)) { + HANDLE h = OpenProcess(PROCESS_CREATE_THREAD | + PROCESS_QUERY_INFORMATION | + PROCESS_VM_OPERATION | PROCESS_VM_WRITE | + PROCESS_VM_READ | PROCESS_TERMINATE, + FALSE, pid); + int ret; + + if (h) + ret = exit_process(h, 128 + sig); + else { + h = OpenProcess(PROCESS_TERMINATE, FALSE, pid); + if (!h) { + errno = err_win_to_posix(GetLastError()); + return -1; + } + ret = terminate_process_tree(h, 128 + sig); + } + if (ret) { + errno = err_win_to_posix(GetLastError()); CloseHandle(h); - return 0; } - - errno = err_win_to_posix(GetLastError()); - CloseHandle(h); - return -1; + return ret; } else if (pid > 0 && sig == 0) { HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (h) { diff --git a/compat/win32/exit-process.h b/compat/win32/exit-process.h new file mode 100644 index 00000000000000..d53989884cfb0c --- /dev/null +++ b/compat/win32/exit-process.h @@ -0,0 +1,165 @@ +#ifndef EXIT_PROCESS_H +#define EXIT_PROCESS_H + +/* + * This file contains functions to terminate a Win32 process, as gently as + * possible. + * + * At first, we will attempt to inject a thread that calls ExitProcess(). If + * that fails, we will fall back to terminating the entire process tree. + * + * For simplicity, these functions are marked as file-local. + */ + +#include + +/* + * Terminates the process corresponding to the process ID and all of its + * directly and indirectly spawned subprocesses. + * + * This way of terminating the processes is not gentle: the processes get + * no chance of cleaning up after themselves (closing file handles, removing + * .lock files, terminating spawned processes (if any), etc). + */ +static int terminate_process_tree(HANDLE main_process, int exit_status) +{ + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 entry; + DWORD pids[16384]; + int max_len = sizeof(pids) / sizeof(*pids), i, len, ret = 0; + pid_t pid = GetProcessId(main_process); + + pids[0] = (DWORD)pid; + len = 1; + + /* + * Even if Process32First()/Process32Next() seem to traverse the + * processes in topological order (i.e. parent processes before + * child processes), there is nothing in the Win32 API documentation + * suggesting that this is guaranteed. + * + * Therefore, run through them at least twice and stop when no more + * process IDs were added to the list. + */ + for (;;) { + int orig_len = len; + + memset(&entry, 0, sizeof(entry)); + entry.dwSize = sizeof(entry); + + if (!Process32First(snapshot, &entry)) + break; + + do { + for (i = len - 1; i >= 0; i--) { + if (pids[i] == entry.th32ProcessID) + break; + if (pids[i] == entry.th32ParentProcessID) + pids[len++] = entry.th32ProcessID; + } + } while (len < max_len && Process32Next(snapshot, &entry)); + + if (orig_len == len || len >= max_len) + break; + } + + for (i = len - 1; i > 0; i--) { + HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, pids[i]); + + if (process) { + if (!TerminateProcess(process, exit_status)) + ret = -1; + CloseHandle(process); + } + } + if (!TerminateProcess(main_process, exit_status)) + ret = -1; + CloseHandle(main_process); + + return ret; +} + +/** + * Determine whether a process runs in the same architecture as the current + * one. That test is required before we assume that GetProcAddress() returns + * a valid address *for the target process*. + */ +static inline int process_architecture_matches_current(HANDLE process) +{ + static BOOL current_is_wow = -1; + BOOL is_wow; + + if (current_is_wow == -1 && + !IsWow64Process (GetCurrentProcess(), ¤t_is_wow)) + current_is_wow = -2; + if (current_is_wow == -2) + return 0; /* could not determine current process' WoW-ness */ + if (!IsWow64Process (process, &is_wow)) + return 0; /* cannot determine */ + return is_wow == current_is_wow; +} + +/** + * Inject a thread into the given process that runs ExitProcess(). + * + * Note: as kernel32.dll is loaded before any process, the other process and + * this process will have ExitProcess() at the same address. + * + * This function expects the process handle to have the access rights for + * CreateRemoteThread(): PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, + * PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ. + * + * The idea comes from the Dr Dobb's article "A Safer Alternative to + * TerminateProcess()" by Andrew Tucker (July 1, 1999), + * http://www.drdobbs.com/a-safer-alternative-to-terminateprocess/184416547 + * + * If this method fails, we fall back to running terminate_process_tree(). + */ +static int exit_process(HANDLE process, int exit_code) +{ + DWORD code; + + if (GetExitCodeProcess(process, &code) && code == STILL_ACTIVE) { + static int initialized; + static LPTHREAD_START_ROUTINE exit_process_address; + PVOID arg = (PVOID)(intptr_t)exit_code; + DWORD thread_id; + HANDLE thread = NULL; + + if (!initialized) { + HINSTANCE kernel32 = GetModuleHandleA("kernel32"); + if (!kernel32) + die("BUG: cannot find kernel32"); + exit_process_address = + (LPTHREAD_START_ROUTINE)(void (*)(void)) + GetProcAddress(kernel32, "ExitProcess"); + initialized = 1; + } + if (!exit_process_address || + !process_architecture_matches_current(process)) + return terminate_process_tree(process, exit_code); + + thread = CreateRemoteThread(process, NULL, 0, + exit_process_address, + arg, 0, &thread_id); + if (thread) { + CloseHandle(thread); + /* + * If the process survives for 10 seconds (a completely + * arbitrary value picked from thin air), fall back to + * killing the process tree via TerminateProcess(). + */ + if (WaitForSingleObject(process, 10000) == + WAIT_OBJECT_0) { + CloseHandle(process); + return 0; + } + } + + return terminate_process_tree(process, exit_code); + } + + return 0; +} + +#endif From 363f1d8b3a9c90f13ca6fd9ab0dc47e483e3cc9c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Jun 2026 16:24:20 +0000 Subject: [PATCH 087/436] mingw: really handle SIGINT Previously, we did not install any handler for Ctrl+C, but now we really want to because the MSYS2 runtime learned the trick to call the ConsoleCtrlHandler when Ctrl+C was pressed. With this, hitting Ctrl+C while `git log` is running will only terminate the Git process, but not the pager. This finally matches the behavior on Linux and on macOS. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/mingw.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index 44c63059cd6916..41e055f7de885e 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -3623,7 +3623,14 @@ static void adjust_symlink_flags(void) symlink_file_flags |= 2; symlink_directory_flags |= 2; } +} +static BOOL WINAPI handle_ctrl_c(DWORD ctrl_type) +{ + if (ctrl_type != CTRL_C_EVENT) + return FALSE; /* we did not handle this */ + mingw_raise(SIGINT); + return TRUE; /* we did handle this */ } #ifdef _MSC_VER @@ -3660,6 +3667,8 @@ int wmain(int argc, const wchar_t **wargv) #endif #endif + SetConsoleCtrlHandler(handle_ctrl_c, TRUE); + maybe_redirect_std_handles(); adjust_symlink_flags(); From 4a1eb9304aae95ca52dff72a099e060dd6a1b8c9 Mon Sep 17 00:00:00 2001 From: Lucas Seiki Oshiro Date: Thu, 4 Jun 2026 13:34:42 -0300 Subject: [PATCH 088/436] Documentation: remove redundant 'instead' in --subject-prefix The documentation for --subject-prefix has two words "instead" in the same sentence, making it a little bit confusing to read. Change the order of the phrase to a more natural "Use [...] instead of [...]" structure. Signed-off-by: Lucas Seiki Oshiro Signed-off-by: Junio C Hamano --- Documentation/git-format-patch.adoc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Documentation/git-format-patch.adoc b/Documentation/git-format-patch.adoc index 5662382450289a..f7905c0f7c0322 100644 --- a/Documentation/git-format-patch.adoc +++ b/Documentation/git-format-patch.adoc @@ -221,10 +221,9 @@ populated with placeholder text. for generating the cover letter. --subject-prefix=:: - Instead of the standard '[PATCH]' prefix in the subject - line, instead use '[]'. This can be used - to name a patch series, and can be combined with the - `--numbered` option. + Use '[]' instead of the standard '[PATCH]' + prefix in the subject line. This can be used to name a patch + series, and can be combined with the `--numbered` option. + The configuration variable `format.subjectPrefix` may also be used to configure a subject prefix to apply to a given repository for From d1b72b29e993ece28ace1f7f5d587e959e26c65c Mon Sep 17 00:00:00 2001 From: Alexander Monakov Date: Fri, 5 Jun 2026 20:26:43 +0300 Subject: [PATCH 089/436] doc: fix typo in GIT_ALTERNATE_OBJECT_DIRECTORIES One file accidentally spelled GIT_ALTERNATE_OBJECT_DIRECTORIES with REPOSITORIES instead of DIRECTORIES. Fix the typo. Signed-off-by: Alexander Monakov Signed-off-by: Junio C Hamano --- Documentation/technical/hash-function-transition.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/technical/hash-function-transition.adoc b/Documentation/technical/hash-function-transition.adoc index 2359d7d106f842..241d2f763dd436 100644 --- a/Documentation/technical/hash-function-transition.adoc +++ b/Documentation/technical/hash-function-transition.adoc @@ -545,7 +545,7 @@ Alternates ~~~~~~~~~~ For the same reason, a SHA-256 repository cannot borrow objects from a SHA-1 repository using objects/info/alternates or -$GIT_ALTERNATE_OBJECT_REPOSITORIES. +$GIT_ALTERNATE_OBJECT_DIRECTORIES. git notes ~~~~~~~~~ From 3372505ee84ac7f290863c8c7bb0d0dc74c35227 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 4 Jun 2026 06:48:50 +0000 Subject: [PATCH 090/436] git-gui: silence install recipes under "make -s" Several install and uninstall recipes embed "echo" calls that fire as part of the recipe itself, so the install banners (DEST, INSTALL, LINK, REMOVE) were visible whenever the variables expand non-empty. Guard the whole "ifndef V" block on "-s" so the loud variants are selected only when "-s" is absent and V=1 is unset. The existing "-s" check also had its findstring arguments in the wrong order (needle "-s" never fit in haystack "s"), so swap them while moving the check to wrap the block. Signed-off-by: Harald Nordgren Signed-off-by: Johannes Sixt --- Makefile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ca01068810bbe6..d33204e875359c 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ REMOVE_F0 = $(RM_RF) # space is required here REMOVE_F1 = CLEAN_DST = true +ifneq ($(findstring s,$(firstword -$(MAKEFLAGS))),s) ifndef V QUIET = @ QUIET_GEN = $(QUIET)echo ' ' GEN '$@' && @@ -89,6 +90,7 @@ ifndef V REMOVE_F0 = dst= REMOVE_F1 = && echo ' ' REMOVE `basename "$$dst"` && $(RM_RF) "$$dst" endif +endif TCLTK_PATH ?= wish ifeq (./,$(dir $(TCLTK_PATH))) @@ -97,10 +99,6 @@ else TCL_PATH ?= $(dir $(TCLTK_PATH))$(notdir $(subst wish,tclsh,$(TCLTK_PATH))) endif -ifeq ($(findstring $(firstword -$(MAKEFLAGS)),s),s) -QUIET_GEN = -endif - -include config.mak DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) From 014c454799dbf281e634823c6134e8e95978df6f Mon Sep 17 00:00:00 2001 From: Andrew Kreimer Date: Sun, 31 May 2026 21:43:58 +0300 Subject: [PATCH 091/436] doc: fix typos via codespell There are some typos in the documentation, comments, etc. Fix them via codespell, and then adjust the "dump" files used by the subversion tests to match the updated contents. Signed-off-by: Andrew Kreimer [dscho noticed and fixed the problems in svn test] Signed-off-by: Johannes Schindelin [jc did final assembling of the three patches] Signed-off-by: Junio C Hamano --- Documentation/SubmittingPatches | 2 +- Documentation/git-sparse-checkout.adoc | 2 +- Documentation/technical/build-systems.adoc | 6 +- builtin/pack-objects.c | 2 +- commit-graph.h | 2 +- compat/precompose_utf8.c | 2 +- hook.h | 2 +- meson_options.txt | 2 +- midx-write.c | 2 +- odb/source.h | 2 +- packfile.h | 2 +- path.h | 2 +- reftable/system.h | 2 +- t/README | 2 +- t/chainlint.pl | 2 +- t/chainlint/chain-break-false.expect | 2 +- t/chainlint/chain-break-false.test | 2 +- t/t1700-split-index.sh | 2 +- t/t3909-stash-pathspec-file.sh | 6 +- t/t4052-stat-output.sh | 2 +- t/t4067-diff-partial-clone.sh | 2 +- t/t9150/svk-merge.dump | 20 +++---- t/t9151/svn-mergeinfo.dump | 66 +++++++++++----------- t/unit-tests/clar/README.md | 2 +- 24 files changed, 69 insertions(+), 69 deletions(-) diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index d570184ec84998..35b4952c8a8d27 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -92,7 +92,7 @@ input and avoids unnecessary churn from many rapid iterations. topic are appropriate, so such an incremental updates are limited to small corrections and polishing. After a topic cooks for some time (like 7 calendar days) in 'next' without needing further tweaks on - top, it gets merged to the 'master' branch and wait to become part + top, it gets merged to the 'master' branch and waits to become part of the next major release. In the following sections, many techniques and conventions are listed diff --git a/Documentation/git-sparse-checkout.adoc b/Documentation/git-sparse-checkout.adoc index 0d1618f161ed63..e286584c67f98f 100644 --- a/Documentation/git-sparse-checkout.adoc +++ b/Documentation/git-sparse-checkout.adoc @@ -134,7 +134,7 @@ the `clean.requireForce` config option is set to `false`. + The `--dry-run` option will list the directories that would be removed without deleting them. Running in this mode can be helpful to predict the -behavior of the clean comand or to determine which kinds of files are left +behavior of the clean command or to determine which kinds of files are left in the sparse directories. + The `--verbose` option will list every file within the directories that diff --git a/Documentation/technical/build-systems.adoc b/Documentation/technical/build-systems.adoc index 3c5237b9fd4727..ca5b5d96f149ba 100644 --- a/Documentation/technical/build-systems.adoc +++ b/Documentation/technical/build-systems.adoc @@ -47,7 +47,7 @@ Auto-detection of the following items is considered to be important: - Check for the existence of headers. - Check for the existence of libraries. - - Check for the existence of exectuables. + - Check for the existence of executables. - Check for the runtime behavior of specific functions. - Check for specific link order requirements when multiple libraries are involved. @@ -106,7 +106,7 @@ by the build system: - C: the primary compiled language used by Git, must be supported. Relevant toolchains are GCC, Clang and MSVC. - - Rust: candidate as a second compiled lanugage, should be supported. Relevant + - Rust: candidate as a second compiled language, should be supported. Relevant toolchains is the LLVM-based rustc. Built-in support for the respective languages is preferred over support that @@ -142,7 +142,7 @@ The following list of build systems are considered: === GNU Make -- Platform support: ubitquitous on all platforms, but not well-integrated into Windows. +- Platform support: ubiquitous on all platforms, but not well-integrated into Windows. - Auto-detection: no built-in support for auto-detection of features. - Ease of use: easy to use, but discovering available options is hard. Makefile rules can quickly get out of hand once reaching a certain scope. diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index dd2480a73d2edf..806068907e79b9 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -1341,7 +1341,7 @@ static void write_pack_file(void) * length of them as buffer length. * * Note that we need to subtract one though to - * accomodate for the sideband byte. + * accommodate for the sideband byte. */ struct hashfd_options opts = { .progress = progress_state, diff --git a/commit-graph.h b/commit-graph.h index f6a54336415453..13ca4ff010fa18 100644 --- a/commit-graph.h +++ b/commit-graph.h @@ -18,7 +18,7 @@ * This method is only used to enhance coverage of the commit-graph * feature in the test suite with the GIT_TEST_COMMIT_GRAPH and * GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS environment variables. Do not - * call this method oustide of a builtin, and only if you know what + * call this method outside of a builtin, and only if you know what * you are doing! */ void git_test_write_commit_graph_or_die(struct odb_source *source); diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c index 43b3be011439ef..6e709bd1384cab 100644 --- a/compat/precompose_utf8.c +++ b/compat/precompose_utf8.c @@ -85,7 +85,7 @@ const char *precompose_string_if_needed(const char *in) out = reencode_string_iconv(in, inlen, ic_prec, 0, &outlen); if (out) { if (outlen == inlen && !memcmp(in, out, outlen)) - free(out); /* no need to return indentical */ + free(out); /* no need to return identical */ else in = out; } diff --git a/hook.h b/hook.h index 5c5628dd1f822c..5f0c3f19bb9cc8 100644 --- a/hook.h +++ b/hook.h @@ -116,7 +116,7 @@ struct run_hooks_opt { * While the callback allows piecemeal writing, it can also be * used for smaller inputs, where it gets called only once. * - * Add hook callback initalization context to `feed_pipe_ctx`. + * Add hook callback initialization context to `feed_pipe_ctx`. * Add hook callback internal state to `feed_pipe_cb_data`. * */ diff --git a/meson_options.txt b/meson_options.txt index 659cbb218f46e0..1ed228d42ad7cd 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -106,7 +106,7 @@ option('highlight_bin', type: 'string', value: 'highlight') # Documentation. option('docs', type: 'array', choices: ['man', 'html'], value: [], - description: 'Which documenattion formats to build and install.') + description: 'Which documentation formats to build and install.') option('default_help_format', type: 'combo', choices: ['man', 'html', 'platform'], value: 'platform', description: 'Default format used when executing git-help(1).') option('docs_backend', type: 'combo', choices: ['asciidoc', 'asciidoctor', 'auto'], value: 'auto', diff --git a/midx-write.c b/midx-write.c index a25cab75abad11..5a9756043650a9 100644 --- a/midx-write.c +++ b/midx-write.c @@ -1438,7 +1438,7 @@ static int write_midx_internal(struct write_midx_opts *opts) /* * Attempt opening the pack index to populate num_objects. - * Ignore failiures as they can be expected and are not + * Ignore failures as they can be expected and are not * fatal during this selection time. */ open_pack_index(oldest); diff --git a/odb/source.h b/odb/source.h index f706e0608a4855..4958a503cfe2b9 100644 --- a/odb/source.h +++ b/odb/source.h @@ -338,7 +338,7 @@ static inline int odb_source_read_object_stream(struct odb_read_stream **out, * are only iterated over once. * * The optional `request` structure serves as a template for retrieving the - * object info for each indvidual iterated object and will be populated as if + * object info for each individual iterated object and will be populated as if * `odb_source_read_object_info()` was called on the object. It will not be * modified, the callback will instead be invoked with a separate `struct * object_info` for every object. Object info will not be read when passing a diff --git a/packfile.h b/packfile.h index 9b647da7dda7c1..6dea707ba42af1 100644 --- a/packfile.h +++ b/packfile.h @@ -124,7 +124,7 @@ struct packfile_store { * that packs that contain a lot of accessed objects will be located * towards the front. * - * This is usually desireable, but there are exceptions. One exception + * This is usually desirable, but there are exceptions. One exception * is when the looking up multiple objects in a loop for each packfile. * In that case, we may easily end up with an infinite loop as the * packfiles get reordered to the front repeatedly. diff --git a/path.h b/path.h index 0434ba5e07e806..4c2958a9037179 100644 --- a/path.h +++ b/path.h @@ -217,7 +217,7 @@ void safe_create_dir(struct repository *repo, const char *dir, int share); * * - It always adjusts shared permissions. * - * Returns a negative erorr code on error, 0 on success. + * Returns a negative error code on error, 0 on success. */ int safe_create_dir_in_gitdir(struct repository *repo, const char *path); diff --git a/reftable/system.h b/reftable/system.h index c0e2cbe0ffb90c..628232a46f31f6 100644 --- a/reftable/system.h +++ b/reftable/system.h @@ -84,7 +84,7 @@ struct reftable_flock { * to acquire the lock. If `timeout_ms` is 0 we don't wait, if it is negative * we block indefinitely. * - * Retrun 0 on success, a reftable error code on error. Specifically, + * Return 0 on success, a reftable error code on error. Specifically, * `REFTABLE_LOCK_ERROR` should be returned in case the target path is already * locked. */ diff --git a/t/README b/t/README index adbbd9acf4ab27..085921be4b6c2a 100644 --- a/t/README +++ b/t/README @@ -972,7 +972,7 @@ see test-lib-functions.sh for the full list and their options. - test_lazy_prereq