* [PATCH] refs: support migration with worktrees
@ 2025-10-27 18:26 Sam Bostock via GitGitGadget
2025-10-28 7:28 ` Patrick Steinhardt
0 siblings, 1 reply; 8+ messages in thread
From: Sam Bostock via GitGitGadget @ 2025-10-27 18:26 UTC (permalink / raw)
To: git; +Cc: Sam Bostock, Sam Bostock
From: Sam Bostock <sam.bostock@shopify.com>
Remove the worktree limitation from `git refs migrate` by implementing
migration support for repositories with linked worktrees.
Previously, attempting to migrate a repository with worktrees would fail
with "migrating repositories with worktrees is not supported yet". This
limitation existed because each worktree has its own ref storage that
needed to be migrated separately.
Migration now uses a multi-phase approach to safely handle multiple
worktrees:
1. Phase 1: Iterate through all worktrees and create temporary new ref
storage for each in a staging directory.
2. Phase 2: For each worktree, backup the existing ref storage, then
move the new storage into place.
3. Phase 3: Update the repository format config, clear cached ref stores,
and delete all backups. On failure, restore from backups and report
where the migrated refs can be found for manual recovery.
This approach ensures that if migration fails partway through, the
repository can be restored to its original state.
Key implementation details:
- For files backend: Create a commondir file in temp directories for
linked worktrees so the files backend knows where the common git
directory is located.
- For linked worktrees: Use non-INITIAL transactions to avoid creating
packed-refs files (linked worktrees should never have packed-refs).
- Filter refs during iteration: Linked worktrees only migrate their
per-worktree refs (refs/bisect/*, refs/rewritten/*, refs/worktree/*).
Shared refs are migrated once in the main worktree.
- Write per-worktree refs as loose files: The files backend's
transaction_finish_initial() optimization writes most refs to
packed-refs, but per-worktree refs must be stored as loose files
to maintain proper worktree isolation.
- Backup root refs: During Phase 2, backup all root refs (HEAD,
ORIG_HEAD, etc.) by iterating files in the git directory and using
is_root_ref() to identify them. This ensures safe rollback if
migration fails.
Tests are updated to expect migration with worktrees to succeed, and
new tests verify:
- Basic worktree migration in both directions (files ↔ reftable)
- Migration with multiple worktrees
- Dry-run mode with worktrees
- Physical separation of per-worktree refs
- Bare repository with worktrees
Documentation is updated to reflect the worktree support and note that
migration must be run from the main worktree.
As the author's familiarity with git internals and C is limited, this
change was made with the assistance of Claude Code. However, the author
has carefully reviewed and iterated on the work to ensure quality to the
best of their ability.
Signed-off-by: Sam Bostock <sam.bostock@shopify.com>
Co-Authored-By: Claude <noreply@anthropic.com>
---
Teach git refs migrate to support worktrees
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2077%2Fsambostock%2Frefs-migrate-worktree-support-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2077/sambostock/refs-migrate-worktree-support-v1
Pull-Request: https://github.com/git/git/pull/2077
Documentation/git-refs.adoc | 5 +-
refs.c | 726 ++++++++++++++++++++++++++++--------
refs/files-backend.c | 6 +-
t/t1460-refs-migrate.sh | 446 +++++++++++++++++++++-
4 files changed, 1023 insertions(+), 160 deletions(-)
diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
index fa33680cc7..f6a3bf4f03 100644
--- a/Documentation/git-refs.adoc
+++ b/Documentation/git-refs.adoc
@@ -30,7 +30,8 @@ COMMANDS
--------
`migrate`::
- Migrate ref store between different formats.
+ Migrate ref store between different formats. Supports repositories
+ with worktrees; migration must be run from the main worktree.
`verify`::
Verify reference database consistency.
@@ -95,7 +96,7 @@ KNOWN LIMITATIONS
The ref format migration has several known limitations in its current form:
-* It is not possible to migrate repositories that have worktrees.
+* Migration must be run from the main worktree.
* There is no way to block concurrent writes to the repository during an
ongoing migration. Concurrent writes can lead to an inconsistent migrated
diff --git a/refs.c b/refs.c
index 965381367e..0834329e5a 100644
--- a/refs.c
+++ b/refs.c
@@ -5,6 +5,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "git-compat-util.h"
+#include "abspath.h"
#include "advice.h"
#include "config.h"
#include "environment.h"
@@ -28,6 +29,7 @@
#include "setup.h"
#include "sigchain.h"
#include "date.h"
+#include "dir.h"
#include "commit.h"
#include "wildmatch.h"
#include "ident.h"
@@ -2974,8 +2976,296 @@ struct migration_data {
struct strbuf *errbuf;
struct strbuf sb, name, mail;
uint64_t index;
+ int is_main_worktree;
};
+/*
+ * Holds the state for migrating a single worktree's ref storage.
+ */
+struct worktree_migration_data {
+ struct worktree *worktree;
+ struct ref_store *old_refs;
+ struct ref_store *new_refs;
+ struct strbuf new_dir;
+ struct strbuf backup_dir;
+ int is_main;
+};
+
+static void worktree_migration_data_release(struct worktree_migration_data *wt_data)
+{
+ if (wt_data->new_refs) {
+ ref_store_release(wt_data->new_refs);
+ FREE_AND_NULL(wt_data->new_refs);
+ }
+ strbuf_release(&wt_data->new_dir);
+ strbuf_release(&wt_data->backup_dir);
+}
+
+static void worktree_migration_data_array_release(struct worktree_migration_data *wt_data,
+ size_t nr)
+{
+ for (size_t i = 0; i < nr; i++)
+ worktree_migration_data_release(&wt_data[i]);
+ free(wt_data);
+}
+
+/*
+ * Create a commondir file in the temporary migration directory for a linked
+ * worktree. The files backend needs this to locate the common git directory.
+ * Returns 0 on success, -1 on failure.
+ */
+static int create_commondir_file(const char *new_dir, const char *worktree_path,
+ struct strbuf *errbuf)
+{
+ struct strbuf commondir_path = STRBUF_INIT;
+ struct strbuf commondir_content = STRBUF_INIT;
+ int fd = -1;
+ int ret = 0;
+
+ strbuf_addf(&commondir_path, "%s/commondir", new_dir);
+ strbuf_addstr(&commondir_content, "../..\n");
+
+ fd = open(commondir_path.buf, O_WRONLY | O_CREAT | O_EXCL, 0666);
+ if (fd < 0) {
+ strbuf_addf(errbuf, _("cannot create commondir file for worktree '%s': %s"),
+ worktree_path, strerror(errno));
+ ret = -1;
+ goto done;
+ }
+
+ if (write_in_full(fd, commondir_content.buf, commondir_content.len) < 0) {
+ strbuf_addf(errbuf, _("cannot write commondir file for worktree '%s': %s"),
+ worktree_path, strerror(errno));
+ ret = -1;
+ goto done;
+ }
+
+done:
+ if (fd >= 0)
+ close(fd);
+ strbuf_release(&commondir_path);
+ strbuf_release(&commondir_content);
+ return ret;
+}
+
+/*
+ * Returns the list of ref storage items to backup/restore for a worktree.
+ * Main worktrees include packed-refs, linked worktrees do not.
+ */
+static const char **get_ref_storage_items(int is_main_worktree)
+{
+ static const char *main_items[] = {"refs", "logs", "reftable", "packed-refs", NULL};
+ static const char *linked_items[] = {"refs", "logs", "reftable", NULL};
+
+ return is_main_worktree ? main_items : linked_items;
+}
+
+/*
+ * Move root ref files from one directory to another. Root refs are individual
+ * files in the git directory like HEAD, ORIG_HEAD, etc. If remove_dest is set,
+ * unlink the destination file before moving.
+ *
+ * Returns -1 on fatal error (cannot open source directory), 0 on success,
+ * or positive count of files that failed to move. Detailed error messages
+ * are appended to errbuf if provided.
+ */
+static int move_root_refs(const char *from_dir, const char *to_dir,
+ int remove_dest, struct strbuf *errbuf)
+{
+ struct strbuf from = STRBUF_INIT, to = STRBUF_INIT;
+ DIR *dir = opendir(from_dir);
+ struct dirent *e;
+ int ret = 0;
+ int failed_moves = 0;
+
+ if (!dir) {
+ ret = -1;
+ goto done;
+ }
+
+ while ((e = readdir(dir))) {
+ struct stat st;
+
+ if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, ".."))
+ continue;
+
+ /* Only process files that are root refs */
+ if (!is_root_ref(e->d_name))
+ continue;
+
+ strbuf_reset(&from);
+ strbuf_addf(&from, "%s/%s", from_dir, e->d_name);
+
+ if (stat(from.buf, &st) < 0) {
+ if (errno != ENOENT && errbuf) {
+ strbuf_addf(errbuf, _("could not stat '%s': %s; "),
+ from.buf, strerror(errno));
+ failed_moves++;
+ }
+ continue;
+ }
+
+ if (!S_ISREG(st.st_mode))
+ continue; /* skip non-files */
+
+ strbuf_reset(&to);
+ strbuf_addf(&to, "%s/%s", to_dir, e->d_name);
+
+ if (remove_dest) {
+ if (unlink(to.buf) < 0 && errno != ENOENT && errbuf) {
+ strbuf_addf(errbuf, _("could not unlink '%s': %s; "),
+ to.buf, strerror(errno));
+ failed_moves++;
+ continue;
+ }
+ }
+
+ if (rename(from.buf, to.buf) < 0) {
+ if (errbuf) {
+ strbuf_addf(errbuf, _("could not move '%s' to '%s': %s; "),
+ from.buf, to.buf, strerror(errno));
+ failed_moves++;
+ }
+ }
+ }
+ closedir(dir);
+ ret = failed_moves;
+
+done:
+ strbuf_release(&from);
+ strbuf_release(&to);
+ return ret;
+}
+
+/*
+ * Backup ref storage by moving ref-related files/directories to a backup
+ * location. Returns 0 on success, -1 on failure.
+ */
+static int backup_ref_storage(const char *gitdir, const char *backup_dir,
+ int is_main_worktree, struct strbuf *errbuf)
+{
+ struct strbuf from = STRBUF_INIT, to = STRBUF_INIT;
+ const char **items = get_ref_storage_items(is_main_worktree);
+ size_t i;
+ int ret = 0;
+
+ /*
+ * Move ref-related files and directories. Not all will exist depending
+ * on the backend, which is fine.
+ */
+ for (i = 0; items[i]; i++) {
+ const char *item;
+ struct stat st;
+
+ item = items[i];
+ strbuf_reset(&from);
+ strbuf_addf(&from, "%s/%s", gitdir, item);
+
+ if (stat(from.buf, &st) < 0) {
+ if (errno == ENOENT)
+ continue; /* doesn't exist, skip */
+ strbuf_addf(errbuf, _("could not stat '%s': %s"),
+ from.buf, strerror(errno));
+ ret = -1;
+ goto done;
+ }
+
+ strbuf_reset(&to);
+ strbuf_addf(&to, "%s/%s", backup_dir, item);
+
+ if (rename(from.buf, to.buf) < 0) {
+ strbuf_addf(errbuf, _("could not move '%s' to '%s': %s"),
+ from.buf, to.buf, strerror(errno));
+ ret = -1;
+ goto done;
+ }
+ }
+
+ /* Backup root refs (HEAD, ORIG_HEAD, etc.) */
+ ret = move_root_refs(gitdir, backup_dir, 0, errbuf);
+ if (ret < 0) {
+ strbuf_addf(errbuf, _("could not open directory '%s' to backup root refs: %s"),
+ gitdir, strerror(errno));
+ ret = -1;
+ goto done;
+ } else if (ret > 0) {
+ /* Some root refs failed to backup - this is fatal */
+ strbuf_addstr(errbuf, _("failed to backup some root refs"));
+ ret = -1;
+ goto done;
+ }
+ ret = 0;
+
+done:
+ strbuf_release(&from);
+ strbuf_release(&to);
+ return ret;
+}
+
+/*
+ * Restore ref storage from backup by moving files back.
+ */
+static void restore_ref_storage_from_backup(const char *gitdir,
+ const char *backup_dir,
+ int is_main_worktree)
+{
+ struct strbuf from = STRBUF_INIT, to = STRBUF_INIT;
+ const char **items = get_ref_storage_items(is_main_worktree);
+ size_t i;
+
+ for (i = 0; items[i]; i++) {
+ const char *item;
+ struct stat st;
+
+ item = items[i];
+ strbuf_reset(&from);
+ strbuf_addf(&from, "%s/%s", backup_dir, item);
+
+ if (stat(from.buf, &st) < 0)
+ continue; /* doesn't exist in backup */
+
+ strbuf_reset(&to);
+ strbuf_addf(&to, "%s/%s", gitdir, item);
+
+ /* Remove what's currently there (new storage that failed) */
+ if (stat(to.buf, &st) == 0) {
+ if (S_ISDIR(st.st_mode))
+ remove_dir_recursively(&to, 0);
+ else
+ unlink(to.buf);
+ }
+
+ rename(from.buf, to.buf);
+ }
+
+ /* Restore root refs from backup */
+ {
+ struct strbuf restore_err = STRBUF_INIT;
+ int restore_ret = move_root_refs(backup_dir, gitdir, 1, &restore_err);
+ if (restore_ret < 0) {
+ warning_errno(_("could not open directory '%s' to restore root refs"), backup_dir);
+ } else if (restore_ret > 0) {
+ warning(_("failed to restore some root refs: %s"), restore_err.buf);
+ }
+ strbuf_release(&restore_err);
+ }
+
+ strbuf_release(&from);
+ strbuf_release(&to);
+}
+
+/*
+ * Delete backup directory and its contents.
+ */
+static void delete_backup(const char *backup_dir)
+{
+ struct strbuf path = STRBUF_INIT;
+
+ strbuf_addstr(&path, backup_dir);
+ remove_dir_recursively(&path, 0);
+ strbuf_release(&path);
+}
+
static int migrate_one_ref(const char *refname, const char *referent UNUSED, const struct object_id *oid,
int flags, void *cb_data)
{
@@ -2983,6 +3273,16 @@ static int migrate_one_ref(const char *refname, const char *referent UNUSED, con
struct strbuf symref_target = STRBUF_INIT;
int ret;
+ /*
+ * For linked worktrees, only migrate per-worktree refs. Shared refs
+ * are migrated once in the main worktree.
+ */
+ if (!data->is_main_worktree) {
+ enum ref_worktree_type type = parse_worktree_ref(refname, NULL, NULL, NULL);
+ if (type != REF_WORKTREE_CURRENT)
+ return 0;
+ }
+
if (flags & REF_ISSYMREF) {
ret = refs_read_symbolic_ref(data->old_refs, refname, &symref_target);
if (ret < 0)
@@ -3052,7 +3352,7 @@ static int move_files(const char *from_path, const char *to_path, struct strbuf
from_dir = opendir(from_path);
if (!from_dir) {
- strbuf_addf(errbuf, "could not open source directory '%s': %s",
+ strbuf_addf(errbuf, _("could not open source directory '%s': %s"),
from_path, strerror(errno));
ret = -1;
goto done;
@@ -3086,14 +3386,14 @@ static int move_files(const char *from_path, const char *to_path, struct strbuf
ret = rename(from_buf.buf, to_buf.buf);
if (ret < 0) {
- strbuf_addf(errbuf, "could not link file '%s' to '%s': %s",
+ strbuf_addf(errbuf, _("could not link file '%s' to '%s': %s"),
from_buf.buf, to_buf.buf, strerror(errno));
goto done;
}
}
if (errno) {
- strbuf_addf(errbuf, "could not read entry from directory '%s': %s",
+ strbuf_addf(errbuf, _("could not read entry from directory '%s': %s"),
from_path, strerror(errno));
ret = -1;
goto done;
@@ -3109,211 +3409,339 @@ done:
return ret;
}
-static int has_worktrees(void)
-{
- struct worktree **worktrees = get_worktrees();
- int ret = 0;
- size_t i;
-
- for (i = 0; worktrees[i]; i++) {
- if (is_main_worktree(worktrees[i]))
- continue;
- ret = 1;
- }
-
- free_worktrees(worktrees);
- return ret;
-}
-
int repo_migrate_ref_storage_format(struct repository *repo,
enum ref_storage_format format,
unsigned int flags,
struct strbuf *errbuf)
{
- struct ref_store *old_refs = NULL, *new_refs = NULL;
- struct ref_transaction *transaction = NULL;
- struct strbuf new_gitdir = STRBUF_INIT;
- struct migration_data data = {
- .sb = STRBUF_INIT,
- .name = STRBUF_INIT,
- .mail = STRBUF_INIT,
- };
- int did_migrate_refs = 0;
+ struct worktree **worktrees = NULL;
+ struct worktree_migration_data *wt_migrations = NULL;
+ size_t nr_worktrees = 0;
int ret;
if (repo->ref_storage_format == format) {
- strbuf_addstr(errbuf, "current and new ref storage format are equal");
+ strbuf_addstr(errbuf, _("current and new ref storage format are equal"));
ret = -1;
goto done;
}
- old_refs = get_main_ref_store(repo);
+ /*
+ * Enumerate all worktrees. We use the variant that doesn't try to read
+ * HEAD both because we don't need it (we'll migrate all refs including
+ * HEAD anyway) and to avoid failures if the ref storage is already
+ * inconsistent (e.g., from a previous interrupted migration or corruption).
+ */
+ worktrees = get_worktrees_without_reading_head();
+ for (nr_worktrees = 0; worktrees[nr_worktrees]; nr_worktrees++)
+ ; /* count worktrees */
/*
- * Worktrees complicate the migration because every worktree has a
- * separate ref storage. While it should be feasible to implement, this
- * is pushed out to a future iteration.
- *
- * TODO: we should really be passing the caller-provided repository to
- * `has_worktrees()`, but our worktree subsystem doesn't yet support
- * that.
+ * Migration must be run from the main worktree. When running from a
+ * linked worktree, the_repository context points to the worktree's
+ * gitdir, causing the migration logic to operate on the wrong
+ * directory structure.
*/
- if (has_worktrees()) {
- strbuf_addstr(errbuf, "migrating repositories with worktrees is not supported yet");
- ret = -1;
- goto done;
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ if (worktrees[i]->is_current && !is_main_worktree(worktrees[i])) {
+ strbuf_addf(errbuf, _("migration must be run from the main worktree at %s"),
+ worktrees[0]->path);
+ ret = -1;
+ goto done;
+ }
}
+ CALLOC_ARRAY(wt_migrations, nr_worktrees);
+
/*
* The overall logic looks like this:
*
- * 1. Set up a new temporary directory and initialize it with the new
- * format. This is where all refs will be migrated into.
+ * 1. For each worktree, set up a new temporary directory and
+ * initialize it with the new format. This is where all refs for
+ * that worktree will be migrated into.
*
- * 2. Enumerate all refs and write them into the new ref storage.
- * This operation is safe as we do not yet modify the main
- * repository.
+ * 2. For each worktree, enumerate all refs and write them into the
+ * new ref storage. This operation is safe as we do not yet modify
+ * the main repository.
*
- * 3. Enumerate all reflogs and write them into the new ref storage.
- * This operation is safe as we do not yet modify the main
- * repository.
+ * 3. For each worktree, enumerate all reflogs and write them into
+ * the new ref storage. This operation is safe as we do not yet
+ * modify the main repository.
*
* 4. If we're in dry-run mode then we are done and can hand over the
- * directory to the caller for inspection. If not, we now start
+ * directories to the caller for inspection. If not, we now start
* with the destructive part.
*
- * 5. Delete the old ref storage from disk. As we have a copy of refs
- * in the new ref storage it's okay(ish) if we now get interrupted
- * as there is an equivalent copy of all refs available.
+ * 5. For each worktree, create a backup of the old ref storage by
+ * moving it to a backup location.
+ *
+ * 6. For each worktree, move the new ref storage files into place.
+ * As we have a backup it's okay if we now get interrupted as the
+ * repository can be restored to its original state.
*
- * 6. Move the new ref storage files into place.
+ * 7. Change the repository format to the new ref format. Clear any
+ * cached ref stores so they get reloaded with the new format.
*
- * 7. Change the repository format to the new ref format.
+ * 8. Delete the backup directories.
+ *
+ * All worktrees are processed sequentially. Parallelization would add
+ * complexity for minimal benefit since most repos have few worktrees
+ * and migration is a one-time operation.
*/
- strbuf_addf(&new_gitdir, "%s/%s", old_refs->gitdir, "ref_migration.XXXXXX");
- if (!mkdtemp(new_gitdir.buf)) {
- strbuf_addf(errbuf, "cannot create migration directory: %s",
- strerror(errno));
- ret = -1;
- goto done;
- }
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ struct worktree_migration_data *wt_data = &wt_migrations[i];
+ struct ref_transaction *transaction = NULL;
+ struct migration_data data = {
+ .sb = STRBUF_INIT,
+ .name = STRBUF_INIT,
+ .mail = STRBUF_INIT,
+ };
+ int create_flags = 0;
+
+ wt_data->worktree = worktrees[i];
+ wt_data->is_main = is_main_worktree(worktrees[i]);
+ wt_data->old_refs = get_worktree_ref_store(worktrees[i]);
+ strbuf_init(&wt_data->new_dir, 0);
+ strbuf_init(&wt_data->backup_dir, 0);
+
+ /* Create temporary directory for new ref storage */
+ strbuf_addf(&wt_data->new_dir, "%s/ref_migration.XXXXXX",
+ wt_data->old_refs->gitdir);
+
+ if (!mkdtemp(wt_data->new_dir.buf)) {
+ strbuf_addf(errbuf, _("cannot create migration directory for worktree '%s': %s"),
+ worktrees[i]->path, strerror(errno));
+ ret = -1;
+ goto done;
+ }
- new_refs = ref_store_init(repo, format, new_gitdir.buf,
- REF_STORE_ALL_CAPS);
- ret = ref_store_create_on_disk(new_refs, 0, errbuf);
- if (ret < 0)
- goto done;
+ /*
+ * For linked worktrees migrating to files format, create a commondir
+ * file in the temp directory so the files backend knows where the
+ * common git directory is. Reftable doesn't use commondir files.
+ */
+ if (!wt_data->is_main && format == REF_STORAGE_FORMAT_FILES) {
+ if (create_commondir_file(wt_data->new_dir.buf,
+ worktrees[i]->path, errbuf) < 0) {
+ ret = -1;
+ goto done;
+ }
+ }
- transaction = ref_store_transaction_begin(new_refs, REF_TRANSACTION_FLAG_INITIAL,
- errbuf);
- if (!transaction)
- goto done;
+ /* Initialize new ref store */
+ wt_data->new_refs = ref_store_init(repo, format, wt_data->new_dir.buf,
+ REF_STORE_ALL_CAPS);
- data.old_refs = old_refs;
- data.transaction = transaction;
- data.errbuf = errbuf;
+ /* For linked worktrees, we only need to create the worktree-specific structure */
+ if (!wt_data->is_main)
+ create_flags = REF_STORE_CREATE_ON_DISK_IS_WORKTREE;
- /*
- * We need to use the internal `do_for_each_ref()` here so that we can
- * also include broken refs and symrefs. These would otherwise be
- * skipped silently.
- *
- * Ideally, we would do this call while locking the old ref storage
- * such that there cannot be any concurrent modifications. We do not
- * have the infra for that though, and the "files" backend does not
- * allow for a central lock due to its design. It's thus on the user to
- * ensure that there are no concurrent writes.
- */
- ret = do_for_each_ref(old_refs, "", NULL, migrate_one_ref, 0,
- DO_FOR_EACH_INCLUDE_ROOT_REFS | DO_FOR_EACH_INCLUDE_BROKEN,
- &data);
- if (ret < 0)
- goto done;
+ ret = ref_store_create_on_disk(wt_data->new_refs, create_flags, errbuf);
+ if (ret < 0)
+ goto done;
+
+ /*
+ * Begin transaction for migrating refs. For linked worktrees,
+ * we don't use REF_TRANSACTION_FLAG_INITIAL because that flag
+ * causes refs to be written to packed-refs, which should not
+ * exist in linked worktree directories.
+ */
+ transaction = ref_store_transaction_begin(wt_data->new_refs,
+ wt_data->is_main ? REF_TRANSACTION_FLAG_INITIAL : 0,
+ errbuf);
+ if (!transaction) {
+ ret = -1;
+ goto done;
+ }
+
+ data.old_refs = wt_data->old_refs;
+ data.transaction = transaction;
+ data.errbuf = errbuf;
+ data.is_main_worktree = wt_data->is_main;
+
+ /*
+ * We need to use the internal `do_for_each_ref()` here so that
+ * we can also include broken refs and symrefs. These would
+ * otherwise be skipped silently.
+ *
+ * Ideally, we would do this call while locking the old ref
+ * storage such that there cannot be any concurrent modifications.
+ * We do not have the infra for that though, and the "files"
+ * backend does not allow for a central lock due to its design.
+ * It's thus on the user to ensure that there are no concurrent
+ * writes.
+ */
+ ret = do_for_each_ref(wt_data->old_refs, "", NULL, migrate_one_ref, 0,
+ DO_FOR_EACH_INCLUDE_ROOT_REFS | DO_FOR_EACH_INCLUDE_BROKEN,
+ &data);
+ if (ret < 0) {
+ ref_transaction_free(transaction);
+ strbuf_release(&data.sb);
+ strbuf_release(&data.name);
+ strbuf_release(&data.mail);
+ goto done;
+ }
+
+ if (!(flags & REPO_MIGRATE_REF_STORAGE_FORMAT_SKIP_REFLOG)) {
+ ret = refs_for_each_reflog(wt_data->old_refs, migrate_one_reflog, &data);
+ if (ret < 0) {
+ ref_transaction_free(transaction);
+ strbuf_release(&data.sb);
+ strbuf_release(&data.name);
+ strbuf_release(&data.mail);
+ goto done;
+ }
+ }
+
+ ret = ref_transaction_commit(transaction, errbuf);
+ ref_transaction_free(transaction);
+ strbuf_release(&data.sb);
+ strbuf_release(&data.name);
+ strbuf_release(&data.mail);
- if (!(flags & REPO_MIGRATE_REF_STORAGE_FORMAT_SKIP_REFLOG)) {
- ret = refs_for_each_reflog(old_refs, migrate_one_reflog, &data);
if (ret < 0)
goto done;
- }
- ret = ref_transaction_commit(transaction, errbuf);
- if (ret < 0)
- goto done;
- did_migrate_refs = 1;
+ /*
+ * Linked worktrees should not have a packed-refs file. If one
+ * was created during the transaction, remove it before moving
+ * files into place.
+ */
+ if (!wt_data->is_main) {
+ struct strbuf packed_refs = STRBUF_INIT;
+ strbuf_addf(&packed_refs, "%s/packed-refs", wt_data->new_dir.buf);
+ if (unlink(packed_refs.buf) < 0 && errno != ENOENT)
+ warning_errno(_("could not remove packed-refs from linked worktree at '%s'"),
+ packed_refs.buf);
+ strbuf_release(&packed_refs);
+ }
+
+ /*
+ * Release the new ref store to close any open files. This is
+ * required for platforms like Cygwin where renaming an open
+ * file results in EPERM.
+ */
+ ref_store_release(wt_data->new_refs);
+ FREE_AND_NULL(wt_data->new_refs);
+ }
if (flags & REPO_MIGRATE_REF_STORAGE_FORMAT_DRYRUN) {
- printf(_("Finished dry-run migration of refs, "
- "the result can be found at '%s'\n"), new_gitdir.buf);
+ printf(_("Finished dry-run migration of refs for %"PRIuMAX" worktree(s)\n"),
+ (uintmax_t)nr_worktrees);
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ const char *path = wt_migrations[i].new_dir.buf;
+
+ /* Show absolute paths consistently for both main and linked worktrees */
+ if (!is_absolute_path(path))
+ path = absolute_path(path);
+
+ printf(_(" Worktree '%s': %s\n"),
+ worktrees[i]->path,
+ path);
+ }
ret = 0;
goto done;
}
- /*
- * Release the new ref store such that any potentially-open files will
- * be closed. This is required for platforms like Cygwin, where
- * renaming an open file results in EPERM.
- */
- ref_store_release(new_refs);
- FREE_AND_NULL(new_refs);
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ struct worktree_migration_data *wt_data = &wt_migrations[i];
- /*
- * Until now we were in the non-destructive phase, where we only
- * populated the new ref store. From hereon though we are about
- * to get hands by deleting the old ref store and then moving
- * the new one into place.
- *
- * Assuming that there were no concurrent writes, the new ref
- * store should have all information. So if we fail from hereon
- * we may be in an in-between state, but it would still be able
- * to recover by manually moving remaining files from the
- * temporary migration directory into place.
- */
- ret = ref_store_remove_on_disk(old_refs, errbuf);
- if (ret < 0)
- goto done;
+ /* Create backup directory */
+ strbuf_addf(&wt_data->backup_dir, "%s/ref_migration_backup.XXXXXX",
+ wt_data->old_refs->gitdir);
+ if (!mkdtemp(wt_data->backup_dir.buf)) {
+ strbuf_addf(errbuf, _("cannot create backup directory for worktree '%s': %s"),
+ worktrees[i]->path, strerror(errno));
+ ret = -1;
+ goto done;
+ }
- ret = move_files(new_gitdir.buf, old_refs->gitdir, errbuf);
- if (ret < 0)
- goto done;
+ /* Backup old ref storage by moving it to backup directory */
+ ret = backup_ref_storage(wt_data->old_refs->gitdir,
+ wt_data->backup_dir.buf,
+ wt_data->is_main, errbuf);
+ if (ret < 0) {
+ strbuf_addf(errbuf, _(" (worktree: %s)"), worktrees[i]->path);
+ goto done;
+ }
- if (rmdir(new_gitdir.buf) < 0)
- warning_errno(_("could not remove temporary migration directory '%s'"),
- new_gitdir.buf);
+ /* Move new ref storage into place */
+ ret = move_files(wt_data->new_dir.buf, wt_data->old_refs->gitdir, errbuf);
+ if (ret < 0) {
+ strbuf_addf(errbuf, _(" (worktree: %s)"), worktrees[i]->path);
+ goto done;
+ }
+
+ /* Remove temporary migration directory */
+ if (rmdir(wt_data->new_dir.buf) < 0)
+ warning_errno(_("could not remove temporary migration directory '%s'"),
+ wt_data->new_dir.buf);
+ }
/*
- * We have migrated the repository, so we now need to adjust the
- * repository format so that clients will use the new ref store.
- * We also need to swap out the repository's main ref store.
+ * Update the repository format so that clients will use the new ref
+ * store.
*/
initialize_repository_version(hash_algo_by_ptr(repo->hash_algo), format, 1);
/*
- * Unset the old ref store and release it. `get_main_ref_store()` will
- * make sure to lazily re-initialize the repository's ref store with
- * the new format.
+ * Reinitialize all worktree ref stores with the new format. We release
+ * the old ones and clear cached pointers so they get lazily
+ * reinitialized with the new format.
*/
- ref_store_release(old_refs);
- FREE_AND_NULL(old_refs);
- repo->refs_private = NULL;
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ if (wt_migrations[i].is_main) {
+ /* Main worktree: clear the cached main ref store */
+ if (repo->refs_private) {
+ ref_store_release(repo->refs_private);
+ FREE_AND_NULL(repo->refs_private);
+ }
+ }
+ /* Worktree ref stores will be lazily reinitialized on next access */
+ }
+
+ /* Delete backup directories since migration succeeded */
+ for (size_t i = 0; i < nr_worktrees; i++) {
+ if (wt_migrations[i].backup_dir.len)
+ delete_backup(wt_migrations[i].backup_dir.buf);
+ }
ret = 0;
done:
- if (ret && did_migrate_refs) {
- strbuf_complete(errbuf, '\n');
- strbuf_addf(errbuf, _("migrated refs can be found at '%s'"),
- new_gitdir.buf);
- }
+ if (ret) {
+ /*
+ * Migration failed. Attempt to restore from backups if we made
+ * it to Phase 2.
+ */
+ for (size_t i = 0; wt_migrations && i < nr_worktrees; i++) {
+ if (wt_migrations[i].backup_dir.len) {
+ restore_ref_storage_from_backup(
+ wt_migrations[i].old_refs->gitdir,
+ wt_migrations[i].backup_dir.buf,
+ wt_migrations[i].is_main);
+ delete_backup(wt_migrations[i].backup_dir.buf);
+ }
+ }
- if (new_refs) {
- ref_store_release(new_refs);
- free(new_refs);
+ /*
+ * Report where migrated refs can be found for manual recovery.
+ * We keep these directories as insurance - if the restore failed,
+ * they may be the only way to recover the migrated refs.
+ */
+ for (size_t i = 0; wt_migrations && i < nr_worktrees; i++) {
+ if (wt_migrations[i].new_dir.len) {
+ strbuf_complete(errbuf, '\n');
+ strbuf_addf(errbuf, _("migrated refs for worktree '%s' can be found at '%s'"),
+ worktrees[i]->path, wt_migrations[i].new_dir.buf);
+ }
+ }
}
- ref_transaction_free(transaction);
- strbuf_release(&new_gitdir);
- strbuf_release(&data.sb);
- strbuf_release(&data.name);
- strbuf_release(&data.mail);
+
+ if (wt_migrations)
+ worktree_migration_data_array_release(wt_migrations, nr_worktrees);
+ if (worktrees)
+ free_worktrees(worktrees);
+
return ret;
}
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 8d7007f4aa..6be56274d3 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -3210,11 +3210,13 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,
string_list_append(&refnames_to_check, update->refname);
/*
- * packed-refs don't support symbolic refs, root refs and reflogs,
- * so we have to queue these references via the loose transaction.
+ * packed-refs don't support symbolic refs, root refs, per-worktree
+ * refs, and reflogs, so we have to queue these references via the
+ * loose transaction.
*/
if (update->new_target ||
is_root_ref(update->refname) ||
+ is_per_worktree_ref(update->refname) ||
(update->flags & REF_LOG_ONLY)) {
if (!loose_transaction) {
loose_transaction = ref_store_transaction_begin(&refs->base, 0, err);
diff --git a/t/t1460-refs-migrate.sh b/t/t1460-refs-migrate.sh
index 0e1116a319..2ea8d31361 100755
--- a/t/t1460-refs-migrate.sh
+++ b/t/t1460-refs-migrate.sh
@@ -114,16 +114,40 @@ do
test_cmp expect err
'
- test_expect_success "$from_format -> $to_format: migration with worktree fails" '
+ test_expect_success "$from_format -> $to_format: migration with worktree" '
test_when_finished "rm -rf repo" &&
git init --ref-format=$from_format repo &&
+ test_commit -C repo initial &&
git -C repo worktree add wt &&
- test_must_fail git -C repo refs migrate \
- --ref-format=$to_format 2>err &&
- cat >expect <<-EOF &&
- error: migrating repositories with worktrees is not supported yet
- EOF
- test_cmp expect err
+
+ # Create some refs and reflogs in both worktrees
+ test_commit -C repo second &&
+ git -C repo update-ref refs/heads/from-main HEAD &&
+ git -C repo/wt checkout -b wt-branch &&
+ test_commit -C repo/wt wt-commit &&
+ git -C repo/wt update-ref refs/bisect/wt-ref HEAD &&
+
+ # Capture refs from both worktrees before migration
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname) %(symref)" >expect-main &&
+ git -C repo/wt for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname) %(symref)" >expect-wt &&
+
+ # Perform migration
+ git -C repo refs migrate --ref-format=$to_format &&
+
+ # Verify refs in both worktrees after migration
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname) %(symref)" >actual-main &&
+ git -C repo/wt for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname) %(symref)" >actual-wt &&
+ test_cmp expect-main actual-main &&
+ test_cmp expect-wt actual-wt &&
+
+ # Verify repository format changed
+ git -C repo rev-parse --show-ref-format >actual &&
+ echo "$to_format" >expect &&
+ test_cmp expect actual
'
test_expect_success "$from_format -> $to_format: unborn HEAD" '
@@ -325,4 +349,412 @@ test_expect_success 'migrating from reftable format deletes backend files' '
test_path_is_file repo/.git/packed-refs
'
+test_expect_success 'files -> reftable: migration with multiple worktrees' '
+ test_when_finished "rm -rf repo" &&
+ git init --ref-format=files repo &&
+ test_commit -C repo initial &&
+ git -C repo worktree add wt1 &&
+ git -C repo worktree add wt2 &&
+
+ # Create unique refs in each worktree
+ test_commit -C repo main-commit &&
+ test_commit -C repo/wt1 wt1-commit &&
+ test_commit -C repo/wt2 wt2-commit &&
+ git -C repo update-ref refs/bisect/main-bisect HEAD &&
+ git -C repo/wt1 update-ref refs/bisect/wt1-bisect HEAD &&
+ git -C repo/wt2 update-ref refs/bisect/wt2-bisect HEAD &&
+
+ # Capture state before migration
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-main &&
+ git -C repo/wt1 for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-wt1 &&
+ git -C repo/wt2 for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-wt2 &&
+
+ # Migrate
+ git -C repo refs migrate --ref-format=reftable &&
+
+ # Verify all worktrees still work
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-main &&
+ git -C repo/wt1 for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-wt1 &&
+ git -C repo/wt2 for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-wt2 &&
+ test_cmp expect-main actual-main &&
+ test_cmp expect-wt1 actual-wt1 &&
+ test_cmp expect-wt2 actual-wt2 &&
+
+ # Verify format changed
+ git -C repo rev-parse --show-ref-format >actual &&
+ echo "reftable" >expect &&
+ test_cmp expect actual &&
+
+ # Verify operations still work in all worktrees
+ test_commit -C repo post-migrate-main &&
+ test_commit -C repo/wt1 post-migrate-wt1 &&
+ test_commit -C repo/wt2 post-migrate-wt2
+'
+
+test_expect_success 'files -> reftable: dry-run with worktrees' '
+ test_when_finished "rm -rf repo" &&
+ git init --ref-format=files repo &&
+ test_commit -C repo initial &&
+ git -C repo worktree add wt &&
+
+ git -C repo refs migrate --ref-format=reftable --dry-run >output &&
+ grep "Finished dry-run migration" output &&
+ grep "2 worktree" output &&
+
+ # Format should not have changed
+ git -C repo rev-parse --show-ref-format >actual &&
+ echo "files" >expect &&
+ test_cmp expect actual &&
+
+ # Files backend should still be present
+ test_path_is_file repo/.git/refs/heads/main
+'
+
+test_expect_success 'reftable -> files: migration with worktrees and per-worktree refs' '
+ test_when_finished "rm -rf repo" &&
+ git init --ref-format=reftable repo &&
+ test_commit -C repo initial &&
+ git -C repo worktree add wt &&
+
+ # Create various types of per-worktree refs
+ test_commit -C repo main-work &&
+ git -C repo update-ref refs/bisect/bad HEAD &&
+ git -C repo update-ref refs/rewritten/main HEAD &&
+ git -C repo update-ref refs/worktree/custom HEAD &&
+
+ test_commit -C repo/wt wt-work &&
+ git -C repo/wt update-ref refs/bisect/good HEAD &&
+ git -C repo/wt update-ref refs/rewritten/wt HEAD &&
+ git -C repo/wt update-ref refs/worktree/wt-custom HEAD &&
+
+ # Capture all refs including per-worktree ones
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-main &&
+ git -C repo/wt for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-wt &&
+
+ # Migrate back to files
+ git -C repo refs migrate --ref-format=files &&
+
+ # Verify per-worktree refs are still separate
+ git -C repo for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-main &&
+ git -C repo/wt for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-wt &&
+ test_cmp expect-main actual-main &&
+ test_cmp expect-wt actual-wt &&
+
+ # Verify physical separation of per-worktree refs
+ test_path_is_file repo/.git/refs/bisect/bad &&
+ test_path_is_file repo/.git/worktrees/wt/refs/bisect/good &&
+ test_path_is_missing repo/.git/refs/bisect/good &&
+ test_path_is_missing repo/.git/worktrees/wt/refs/bisect/bad
+'
+
+test_expect_success 'bare repository with worktrees: bidirectional migration' '
+ test_when_finished "rm -rf bare-repo worktrees" &&
+
+ # Create a bare repository
+ git init --bare --ref-format=files bare-repo &&
+
+ # Add worktrees to the bare repository
+ mkdir worktrees &&
+ git -C bare-repo worktree add ../worktrees/main &&
+ git -C bare-repo worktree add ../worktrees/feature &&
+
+ # Create initial commits and refs in main worktree
+ test_commit -C worktrees/main initial &&
+ git -C worktrees/main update-ref refs/heads/main HEAD &&
+ git -C worktrees/main update-ref refs/bisect/main-bad HEAD &&
+ git -C worktrees/main update-ref refs/worktree/main-custom HEAD &&
+
+ # Create commits and refs in feature worktree
+ test_commit -C worktrees/feature feature-work &&
+ git -C worktrees/feature update-ref refs/bisect/feature-bad HEAD &&
+ git -C worktrees/feature update-ref refs/worktree/feature-custom HEAD &&
+
+ # Capture all refs before migration
+ git -C worktrees/main for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-main &&
+ git -C worktrees/feature for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >expect-feature &&
+
+ # Migrate bare repo to reftable
+ git -C bare-repo refs migrate --ref-format=reftable &&
+
+ # Verify format changed
+ git -C bare-repo rev-parse --show-ref-format >actual &&
+ echo "reftable" >expect-format &&
+ test_cmp expect-format actual &&
+
+ # Verify all refs still exist and are correct
+ git -C worktrees/main for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-main &&
+ git -C worktrees/feature for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-feature &&
+ test_cmp expect-main actual-main &&
+ test_cmp expect-feature actual-feature &&
+
+ # Migrate back to files
+ git -C bare-repo refs migrate --ref-format=files &&
+
+ # Verify format changed back
+ git -C bare-repo rev-parse --show-ref-format >actual &&
+ echo "files" >expect-format &&
+ test_cmp expect-format actual &&
+
+ # Verify all refs still exist and are correct after round-trip
+ git -C worktrees/main for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-main &&
+ git -C worktrees/feature for-each-ref --include-root-refs \
+ --format="%(refname) %(objectname)" | sort >actual-feature &&
+ test_cmp expect-main actual-main &&
+ test_cmp expect-feature actual-feature &&
+
+ # Verify physical separation of per-worktree refs
+ test_path_is_file bare-repo/worktrees/main/refs/bisect/main-bad &&
+ test_path_is_file bare-repo/worktrees/feature/refs/bisect/feature-bad &&
+ test_path_is_missing bare-repo/worktrees/main/refs/bisect/feature-bad &&
+ test_path_is_missing bare-repo/worktrees/feature/refs/bisect/main-bad
+'
+
+test_expect_success SANITY 'files -> reftable: migration fails with read-only .git' '
+ test_when_finished "chmod -R u+w read-only-git" &&
+ git init --ref-format=files read-only-git &&
+ test_commit -C read-only-git initial &&
+ chmod -R a-w read-only-git/.git &&
+ test_must_fail git -C read-only-git refs migrate --ref-format=reftable 2>err &&
+ grep -i "permission denied\|read-only" err
+'
+
+test_expect_success SANITY 'files -> reftable: read-only refs directory prevents backup' '
+ test_when_finished "chmod -R u+w read-only-refs" &&
+ git init --ref-format=files read-only-refs &&
+ test_commit -C read-only-refs initial &&
+ chmod a-w read-only-refs/.git/refs &&
+ test_must_fail git -C read-only-refs refs migrate --ref-format=reftable 2>err &&
+ chmod u+w read-only-refs/.git/refs &&
+ grep -i "could not\|permission denied" err
+'
+
+test_expect_success 'files -> reftable: git status works in all worktrees after migration' '
+ test_when_finished "rm -rf repo" &&
+ git init --ref-format=files repo &&
+ test_commit -C repo initial &&
+ git -C repo worktree add wt1 &&
+ git -C repo worktree add wt2 &&
+
+ # Make some commits in each worktree
+ test_commit -C repo main-work &&
+ test_commit -C repo/wt1 wt1-work &&
+ test_commit -C repo/wt2 wt2-work &&
+
+ # Verify status works before migration using -C
+ git -C repo status &&
+ git -C repo/wt1 status &&
+ git -C repo/wt2 status &&
+
+ # Verify status works before migration by cd-ing into worktree
+ (cd repo && git status) &&
+ (cd repo/wt1 && git status) &&
+ (cd repo/wt2 && git status) &&
+
+ # Migrate to reftable
+ git -C repo refs migrate --ref-format=reftable &&
+
+ # Verify status still works after migration using -C
+ git -C repo status &&
+ git -C repo/wt1 status &&
+ git -C repo/wt2 status &&
+
+ # Verify status works after migration by cd-ing into worktree
+ (cd repo && git status) &&
+ (cd repo/wt1 && git status) &&
+ (cd repo/wt2 && git status) &&
+
+ # Verify other common commands work in all worktrees
+ git -C repo log --oneline &&
+ git -C repo/wt1 log --oneline &&
+ git -C repo/wt2 log --oneline &&
+
+ git -C repo branch &&
+ git -C repo/wt1 branch &&
+ git -C repo/wt2 branch &&
+
+ # Migrate back to files
+ git -C repo refs migrate --ref-format=files &&
+
+ # Verify status still works after migrating back
+ git -C repo status &&
+ git -C repo/wt1 status &&
+ git -C repo/wt2 status &&
+
+ (cd repo && git status) &&
+ (cd repo/wt1 && git status) &&
+ (cd repo/wt2 && git status)
+'
+
+test_expect_success 'files -> reftable: migration fails from inside linked worktree' '
+ test_when_finished "rm -rf from-wt-bare.git from-wt-trees" &&
+
+ # Create a bare repo with worktrees
+ git init --bare --ref-format=files from-wt-bare.git &&
+
+ # Add two worktrees
+ mkdir from-wt-trees &&
+ git -C from-wt-bare.git worktree add ../from-wt-trees/wt1 &&
+ git -C from-wt-bare.git worktree add ../from-wt-trees/wt2 &&
+
+ # Create commits in first worktree
+ test_commit -C from-wt-trees/wt1 initial &&
+ test_commit -C from-wt-trees/wt1 second &&
+
+ # Migration from inside a linked worktree should fail with helpful error
+ (
+ cd from-wt-trees/wt1 &&
+ test_must_fail git refs migrate --ref-format=reftable 2>err
+ ) &&
+ grep "migration must be run from the main worktree" from-wt-trees/wt1/err &&
+
+ # Verify repository is not corrupted - refs format should still be files
+ git -C from-wt-bare.git rev-parse --show-ref-format >actual-format &&
+ echo "files" >expect-format &&
+ test_cmp expect-format actual-format &&
+
+ # Verify git status still works in the worktree
+ git -C from-wt-trees/wt1 status &&
+ (cd from-wt-trees/wt1 && git status) &&
+
+ # Verify migration succeeds when run from the main repository
+ git -C from-wt-bare.git refs migrate --ref-format=reftable &&
+
+ # Verify migration actually happened
+ git -C from-wt-bare.git rev-parse --show-ref-format >actual-format-after &&
+ echo "reftable" >expect-format-after &&
+ test_cmp expect-format-after actual-format-after &&
+
+ # Verify worktree still works after successful migration
+ git -C from-wt-trees/wt1 status &&
+ (cd from-wt-trees/wt1 && git status)
+'
+
+test_expect_success 'files -> reftable: migration with uncommitted changes in worktrees' '
+ test_when_finished "rm -rf dirty-wt-repo dirty-wt" &&
+
+ # Create repo with initial commit
+ git init --ref-format=files dirty-wt-repo &&
+ test_commit -C dirty-wt-repo initial &&
+
+ # Create worktree and make a commit there so it has tracked files
+ git -C dirty-wt-repo worktree add ../dirty-wt &&
+ test_commit -C dirty-wt base &&
+
+ # Create uncommitted changes in worktree:
+ # 1. Untracked file
+ echo "untracked content" >dirty-wt/untracked.txt &&
+
+ # 2. Modified tracked file (not staged)
+ echo "modified" >>dirty-wt/base.t &&
+
+ # 3. Staged new file
+ echo "staged new content" >dirty-wt/staged-new.txt &&
+ git -C dirty-wt add staged-new.txt &&
+
+ # 4. Staged modification to tracked file
+ echo "staged modification" >>dirty-wt/initial.t &&
+ git -C dirty-wt add initial.t &&
+
+ # 5. File with both staged AND unstaged changes
+ echo "staged change" >dirty-wt/both.txt &&
+ git -C dirty-wt add both.txt &&
+ echo "unstaged change" >>dirty-wt/both.txt &&
+
+ # Record status before migration
+ git -C dirty-wt status --porcelain >status-before &&
+
+ # Verify status works before migration
+ git -C dirty-wt status &&
+ (cd dirty-wt && git status) &&
+
+ # Migrate from main worktree
+ git -C dirty-wt-repo refs migrate --ref-format=reftable &&
+
+ # Verify migration succeeded
+ git -C dirty-wt-repo rev-parse --show-ref-format >actual &&
+ echo "reftable" >expect &&
+ test_cmp expect actual &&
+
+ # Verify status still works after migration
+ git -C dirty-wt status &&
+ (cd dirty-wt && git status) &&
+
+ # Verify all uncommitted changes are preserved exactly
+ git -C dirty-wt status --porcelain >status-after &&
+ test_cmp status-before status-after &&
+
+ # Verify file contents are preserved
+ test "$(cat dirty-wt/untracked.txt)" = "untracked content" &&
+ grep "modified" dirty-wt/base.t &&
+ test "$(cat dirty-wt/staged-new.txt)" = "staged new content" &&
+ grep "staged modification" dirty-wt/initial.t &&
+ test "$(cat dirty-wt/both.txt)" = "staged change
+unstaged change" &&
+
+ # Verify all 5 types of changes are still present in status
+ test_line_count = 5 status-after
+'
+
+test_expect_success 'files -> reftable: migration with prunable worktree' '
+ test_when_finished "rm -rf prunable-repo" &&
+
+ # Create repo with worktree, then delete the worktree directory
+ git init --ref-format=files prunable-repo &&
+ test_commit -C prunable-repo initial &&
+ git -C prunable-repo worktree add ../prunable-wt &&
+ rm -rf ../prunable-wt &&
+
+ # Migration should still succeed
+ git -C prunable-repo refs migrate --ref-format=reftable &&
+
+ # Verify migration succeeded
+ git -C prunable-repo rev-parse --show-ref-format >actual &&
+ echo "reftable" >expect &&
+ test_cmp expect actual &&
+
+ # Verify worktree is marked as prunable but metadata exists
+ git -C prunable-repo worktree list --porcelain >list &&
+ grep "prunable" list
+'
+
+test_expect_success 'files -> reftable: migration works from main worktree .git directory' '
+ test_when_finished "rm -rf from-gitdir-repo" &&
+
+ git init --ref-format=files from-gitdir-repo &&
+ test_commit -C from-gitdir-repo initial &&
+
+ # Verify status works before migration
+ (cd from-gitdir-repo && git status) &&
+
+ # Run migration from inside .git directory
+ (
+ cd from-gitdir-repo/.git &&
+ git refs migrate --ref-format=reftable
+ ) &&
+
+ # Verify migration succeeded
+ git -C from-gitdir-repo rev-parse --show-ref-format >actual &&
+ echo "reftable" >expect &&
+ test_cmp expect actual &&
+
+ # Verify repo still works
+ git -C from-gitdir-repo status &&
+ (cd from-gitdir-repo && git status)
+'
+
test_done
base-commit: 419c72cb8ada252b260efc38ff91fe201de7c8c3
--
gitgitgadget
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-27 18:26 [PATCH] refs: support migration with worktrees Sam Bostock via GitGitGadget
@ 2025-10-28 7:28 ` Patrick Steinhardt
2025-10-28 16:00 ` Junio C Hamano
0 siblings, 1 reply; 8+ messages in thread
From: Patrick Steinhardt @ 2025-10-28 7:28 UTC (permalink / raw)
To: Sam Bostock via GitGitGadget; +Cc: git, Sam Bostock
On Mon, Oct 27, 2025 at 06:26:20PM +0000, Sam Bostock via GitGitGadget wrote:
> From: Sam Bostock <sam.bostock@shopify.com>
>
> Remove the worktree limitation from `git refs migrate` by implementing
> migration support for repositories with linked worktrees.
>
> Previously, attempting to migrate a repository with worktrees would fail
> with "migrating repositories with worktrees is not supported yet". This
> limitation existed because each worktree has its own ref storage that
> needed to be migrated separately.
>
> Migration now uses a multi-phase approach to safely handle multiple
> worktrees:
>
> 1. Phase 1: Iterate through all worktrees and create temporary new ref
> storage for each in a staging directory.
>
> 2. Phase 2: For each worktree, backup the existing ref storage, then
> move the new storage into place.
>
> 3. Phase 3: Update the repository format config, clear cached ref stores,
> and delete all backups. On failure, restore from backups and report
> where the migrated refs can be found for manual recovery.
Makes sense.
> This approach ensures that if migration fails partway through, the
> repository can be restored to its original state.
>
> Key implementation details:
>
> - For files backend: Create a commondir file in temp directories for
> linked worktrees so the files backend knows where the common git
> directory is located.
Hm, okay. Not yet sure why we need this, but let's read on.
> - For linked worktrees: Use non-INITIAL transactions to avoid creating
> packed-refs files (linked worktrees should never have packed-refs).
Hm, this is unfortunate. The reason why we use initial transactions is
twofold:
- First, we want to avoid creating loose refs, only. This is indeed
something we must not do with worktrees, as you point out.
- But second, we also want to skip pointless checks like the conflict
checks. This results in quite a saving.
Would've been great to retain the second property, but I guess as long
as we only do this for worktrees it's okayish and something we can worry
about at a later point in time. Better to migrate the refs slowish than
not at all.
> - Filter refs during iteration: Linked worktrees only migrate their
> per-worktree refs (refs/bisect/*, refs/rewritten/*, refs/worktree/*).
> Shared refs are migrated once in the main worktree.
Makes sense.
> - Write per-worktree refs as loose files: The files backend's
> transaction_finish_initial() optimization writes most refs to
> packed-refs, but per-worktree refs must be stored as loose files
> to maintain proper worktree isolation.
Isn't this roughly the same as the second bullet point? Feels like they
should be merged together.
> diff --git a/Documentation/git-refs.adoc b/Documentation/git-refs.adoc
> index fa33680cc7..f6a3bf4f03 100644
> --- a/Documentation/git-refs.adoc
> +++ b/Documentation/git-refs.adoc
> @@ -30,7 +30,8 @@ COMMANDS
> --------
>
> `migrate`::
> - Migrate ref store between different formats.
> + Migrate ref store between different formats. Supports repositories
> + with worktrees; migration must be run from the main worktree.
It feels a bit weird to single our worktrees specifically. We don't say
that the tool supports bare and non-bare repositories, either, so the
only reason why we'd have the note about worktrees is historic legacy.
How about this instead:
Migrate ref storage between different formats. Must be run from the
main worktree in case the repository uses worktrees.
> @@ -95,7 +96,7 @@ KNOWN LIMITATIONS
>
> The ref format migration has several known limitations in its current form:
>
> -* It is not possible to migrate repositories that have worktrees.
> +* Migration must be run from the main worktree.
>
I'd drop this bullet point entirely, as I don't really see this as a
limitation anymore.
> diff --git a/refs.c b/refs.c
> index 965381367e..0834329e5a 100644
> --- a/refs.c
> +++ b/refs.c
I'm sorry, but this is _extremely_ hard to review as we're changing
almost all of the implementation at once with random changes left and
right. Furthermore, I feel like we're getting way to intimate with the
different backends here -- that shouldn't be the case though, the logic
that is specific to the backends should really live in the backends
themselves.
The way I'd expect a series like this to look like is to have commits
that:
1. Do preparatory changes, e.g. teach the files backend to not create
a packed-refs file for worktrees during migration.
2. Pull out the logic to migrate a single reference backend that we
already have into a separate function that can be called in a loop.
The end result should be a function that accepts the old refdb as
input and that returns the new refdb.
3. Implement the logic that calls the function we introduced in (2)
for each worktree. This can be done by iterating through all the
worktrees, calling `get_worktree_ref_store()` on it and then
passing the refdb to the new function.
> @@ -2974,8 +2976,296 @@ struct migration_data {
[snip]
> +/*
> + * Create a commondir file in the temporary migration directory for a linked
> + * worktree. The files backend needs this to locate the common git directory.
> + * Returns 0 on success, -1 on failure.
> + */
> +static int create_commondir_file(const char *new_dir, const char *worktree_path,
> + struct strbuf *errbuf)
> +{
I still don't get why we need this. We should have access to both the
ref store of the worktree and the repository, and both of these are
handled in the same process. So there shouldn't be a need to propagate
the commondir via a file.
[snip]
> +/*
> + * Returns the list of ref storage items to backup/restore for a worktree.
> + * Main worktrees include packed-refs, linked worktrees do not.
> + */
> +static const char **get_ref_storage_items(int is_main_worktree)
> +{
> + static const char *main_items[] = {"refs", "logs", "reftable", "packed-refs", NULL};
> + static const char *linked_items[] = {"refs", "logs", "reftable", NULL};
> +
> + return is_main_worktree ? main_items : linked_items;
> +}
This here is what I was referring to as "too intimate with the
backends". This logic should be entirely self-contained in the backends,
and it already is for migrating the main worktree. We have
`ref_store_remove_on_disk()` to prune old data, and as the new ref
storage is written into a temporary directory we don't need to enumerate
its contents, but can instead move all of its entries into the gitdir
directly.
I guess the reason why you have this and all of the following functions
is to create the backups. But that logic must not live in "refs.c", but
it really should live in the backends. I could for example see a new
function that moves a ref store to a different directory.
An alternative would be to not do the backups at all. We only start
doing "destructive" operations when all the new backends have already
been created, so the last step would be to rename everything into place.
If this operation fails or gets cancelled we are left with a broken
repository, true. But the data is not lost, as we can in theory continue
to rename the remaining data into place.
So maybe that's good enough? The user would have to manually restore in
either of the cases, so we don't really gain that much by having a
backup in the first place.
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 8d7007f4aa..6be56274d3 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -3210,11 +3210,13 @@ static int files_transaction_finish_initial(struct files_ref_store *refs,
> string_list_append(&refnames_to_check, update->refname);
>
> /*
> - * packed-refs don't support symbolic refs, root refs and reflogs,
> - * so we have to queue these references via the loose transaction.
> + * packed-refs don't support symbolic refs, root refs, per-worktree
> + * refs, and reflogs, so we have to queue these references via the
> + * loose transaction.
> */
> if (update->new_target ||
> is_root_ref(update->refname) ||
> + is_per_worktree_ref(update->refname) ||
> (update->flags & REF_LOG_ONLY)) {
> if (!loose_transaction) {
> loose_transaction = ref_store_transaction_begin(&refs->base, 0, err);
This is one of these changes where I think it would make sense to split
them out into separate commits so that they can be properly singled out
and explained.
Patrick
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-28 7:28 ` Patrick Steinhardt
@ 2025-10-28 16:00 ` Junio C Hamano
2025-10-29 10:10 ` Patrick Steinhardt
0 siblings, 1 reply; 8+ messages in thread
From: Junio C Hamano @ 2025-10-28 16:00 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Sam Bostock via GitGitGadget, git, Sam Bostock
Patrick Steinhardt <ps@pks.im> writes:
>> `migrate`::
>> - Migrate ref store between different formats.
>> + Migrate ref store between different formats. Supports repositories
>> + with worktrees; migration must be run from the main worktree.
>
> It feels a bit weird to single our worktrees specifically. We don't say
> that the tool supports bare and non-bare repositories, either, so the
> only reason why we'd have the note about worktrees is historic legacy.
> How about this instead:
>
> Migrate ref storage between different formats. Must be run from the
> main worktree in case the repository uses worktrees.
Two thoughts.
* Would it be unacceptable if the primary repository and refstore
uses reftable backend, and a newly attached worktree to the
repository uses ref-files only for its per-worktree refs? If we
should allow it, then "if the ref store you are migrating is in a
repository with multiple worktrees, you must migrate from the
primary and migrate _all_ ref store for all worktrees at once,
into the same backend", which the design of this patch seems to
aim at, would contradict with it, no?
* If "you must do so from the primary worktree and we convert all
the worktrees attached to the same repository" is the only mode
of operation we support (which by the way I have no problem
with---the first bullet point above was asking question, not
suggesting change of design), then would it be easier for the
user to use if the command noticed that it is not in the primary
worktree and switched to it for the user, instead of complaining
and failing?
>> @@ -95,7 +96,7 @@ KNOWN LIMITATIONS
>>
>> The ref format migration has several known limitations in its current form:
>>
>> -* It is not possible to migrate repositories that have worktrees.
>> +* Migration must be run from the main worktree.
>>
>
> I'd drop this bullet point entirely, as I don't really see this as a
> limitation anymore.
I agree that such a limitation should be lifted, but if we have to
say "you must do it this way, not that way", that is still a
limitation ;-).
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-28 16:00 ` Junio C Hamano
@ 2025-10-29 10:10 ` Patrick Steinhardt
2025-10-29 11:33 ` Kristoffer Haugsbakk
2025-10-29 14:47 ` Junio C Hamano
0 siblings, 2 replies; 8+ messages in thread
From: Patrick Steinhardt @ 2025-10-29 10:10 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Sam Bostock via GitGitGadget, git, Sam Bostock
On Tue, Oct 28, 2025 at 09:00:43AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
>
> >> `migrate`::
> >> - Migrate ref store between different formats.
> >> + Migrate ref store between different formats. Supports repositories
> >> + with worktrees; migration must be run from the main worktree.
> >
> > It feels a bit weird to single our worktrees specifically. We don't say
> > that the tool supports bare and non-bare repositories, either, so the
> > only reason why we'd have the note about worktrees is historic legacy.
> > How about this instead:
> >
> > Migrate ref storage between different formats. Must be run from the
> > main worktree in case the repository uses worktrees.
>
> Two thoughts.
>
> * Would it be unacceptable if the primary repository and refstore
> uses reftable backend, and a newly attached worktree to the
> repository uses ref-files only for its per-worktree refs? If we
> should allow it, then "if the ref store you are migrating is in a
> repository with multiple worktrees, you must migrate from the
> primary and migrate _all_ ref store for all worktrees at once,
> into the same backend", which the design of this patch seems to
> aim at, would contradict with it, no?
The problem we have here is backwards compatibility. Right now we assume
that `extensions.refStorage` applies to all worktrees, so if we wanted
to change it like you propose then we'd have to introduce a backwards
incompatible change.
I agree though that it would've been great if we would have said from
the beginning that the worktree-specific configuration is allowed to
override the ref storage format for a worktree. If so, we could easily
convert any of the worktrees (including the main one) by without having
any impact on all the other worktrees.
But we do not live in such a world right now, and getting there would
require some significant reworking of how we handle per-worktree
references. Unfortunate, but I also don't think there's a strong enough
reason to change this.
> * If "you must do so from the primary worktree and we convert all
> the worktrees attached to the same repository" is the only mode
> of operation we support (which by the way I have no problem
> with---the first bullet point above was asking question, not
> suggesting change of design), then would it be easier for the
> user to use if the command noticed that it is not in the primary
> worktree and switched to it for the user, instead of complaining
> and failing?
I'm not sure. The question is whether the user recognizes that migrating
references in the worktree would also migrate references in the main
repository. It might be surprising behaviour if we did that without
asking.
It might of course also be surprising if you do that from the main
working tree. But I think there's an argument to be made that it's at
least _less_ surprising.
> >> @@ -95,7 +96,7 @@ KNOWN LIMITATIONS
> >>
> >> The ref format migration has several known limitations in its current form:
> >>
> >> -* It is not possible to migrate repositories that have worktrees.
> >> +* Migration must be run from the main worktree.
> >>
> >
> > I'd drop this bullet point entirely, as I don't really see this as a
> > limitation anymore.
>
> I agree that such a limitation should be lifted, but if we have to
> say "you must do it this way, not that way", that is still a
> limitation ;-).
So with the above reasoning I'm not sure I'd call this a limitation.
It's rather a mechanism to protect users from unexpected consequences.
Patrick
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-29 10:10 ` Patrick Steinhardt
@ 2025-10-29 11:33 ` Kristoffer Haugsbakk
2025-10-29 16:22 ` Ben Knoble
2025-10-30 6:37 ` Patrick Steinhardt
2025-10-29 14:47 ` Junio C Hamano
1 sibling, 2 replies; 8+ messages in thread
From: Kristoffer Haugsbakk @ 2025-10-29 11:33 UTC (permalink / raw)
To: Patrick Steinhardt, Junio C Hamano; +Cc: Josh Soref, git, Sam Bostock
On Wed, Oct 29, 2025, at 11:10, Patrick Steinhardt wrote:
> On Tue, Oct 28, 2025 at 09:00:43AM -0700, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>>
>> >> `migrate`::
>> >> - Migrate ref store between different formats.
>> >> + Migrate ref store between different formats. Supports repositories
>> >> + with worktrees; migration must be run from the main worktree.
>> >
>> > It feels a bit weird to single our worktrees specifically. We don't say
>> > that the tool supports bare and non-bare repositories, either, so the
>> > only reason why we'd have the note about worktrees is historic legacy.
>> > How about this instead:
>> >
>> > Migrate ref storage between different formats. Must be run from the
>> > main worktree in case the repository uses worktrees.
>>
>> Two thoughts.
>>
>> * Would it be unacceptable if the primary repository and refstore
>> uses reftable backend, and a newly attached worktree to the
>> repository uses ref-files only for its per-worktree refs? If we
>> should allow it, then "if the ref store you are migrating is in a
>> repository with multiple worktrees, you must migrate from the
>> primary and migrate _all_ ref store for all worktrees at once,
>> into the same backend", which the design of this patch seems to
>> aim at, would contradict with it, no?
>
> The problem we have here is backwards compatibility. Right now we assume
> that `extensions.refStorage` applies to all worktrees, so if we wanted
> to change it like you propose then we'd have to introduce a backwards
> incompatible change.
I don’t understand the motivation or use case for supporting different
backends for different worktrees. But Junio would have to explain that.
Maybe the motivation is this weird (from a user’s perspective) limi-
tation that you have to run a command from the main worktree? Okay,
that’s strange but you get the error and switch to wherever the main
worktree is (that the error message hopefully helpfully provides you
with) and run the command there. Then you forget that weird thing five
minutes later since this was a one-off command.
>
> I agree though that it would've been great if we would have said from
> the beginning that the worktree-specific configuration is allowed to
> override the ref storage format for a worktree. If so, we could easily
> convert any of the worktrees (including the main one) by without having
> any impact on all the other worktrees.
As a user I don’t understand why that is a great thing to have.
>
> But we do not live in such a world right now, and getting there would
> require some significant reworking of how we handle per-worktree
> references. Unfortunate, but I also don't think there's a strong enough
> reason to change this.
>
>> * If "you must do so from the primary worktree and we convert all
>> the worktrees attached to the same repository" is the only mode
>> of operation we support (which by the way I have no problem
>> with---the first bullet point above was asking question, not
>> suggesting change of design), then would it be easier for the
>> user to use if the command noticed that it is not in the primary
>> worktree and switched to it for the user, instead of complaining
>> and failing?
>
> I'm not sure. The question is whether the user recognizes that migrating
> references in the worktree would also migrate references in the main
> repository. It might be surprising behaviour if we did that without
> asking.
On the contrary, as a user I think it mattering what worktree I run this
command from sounds very weird. (But again I can tolerate it requiring
me to run it from the main worktree if there are technical difficulties/
limitations. But using different backends for different
worktrees is very weird, again.)
If I run `git gc` I don’t want it to do different things based on what
worktree I am. I want to operate on the repository, and the repository
is the same no matter what worktree I am in. The same principle applies
to this command in my mind.
Is the “main worktree” even something that makes sense from the user’s
perspective? It seems like it’s just a side-effect of the fact that the
repository itself has to live somewhere. Imagine I have one main
worktree and two linked ones. I delete the main worktree. Imagine that
it works because the repository itself is moved to one of the linked
worktrees (arbitrary). Which then becomes the main worktree. But the
user does not have to care as long the user does not poke inside the
`.git` directory. Which the user should not have to do (there should be
commands to answer whatever `.git`-poking motivations).
I am of course not suggesting such a change. But the point is that the
“main worktree” is not such a useful end-user concept.
Sure, I happen to use a “main worktree” in the informal sense that I
often have the original path where I cloned or created the repository
and I have the other ones in satellite locations with more
pointed/topical names (e.g. `git-mine` is the basename of the Git
worktree that I use to `make install`). But I never ever consult `git
worktree list` to remind me what the main worktree is.
Okay. Let’s say I get tripped up by the gitlink or whatever it is kind
of file that worktrees use for `.git`. Because I really want to poke at
the `.git` directory. Then I think “I need to find the main worktree”
because it happens to have the repository and the link to that directory
could not be implemented using a symlink, maybe because of Windows
filesystems, I don’t know. Again a technical limitation to my mind.
No worktree is special except because of technical limitations.
(The “main worktree” even becomes a technically contradictory concept in
the case when the “main worktree” is bare. And that is a popular
practice for some reason.)
And I wonder how many worktree users even actively think about the fact
that per-worktree refs exist. It’s the kind of thing that you have to
logically conclude *has* to be the case:
1. `HEAD` is a ref and you need that for a worktree
2. You can have a bisect session in a worktree and that uses refs under
the hood
But:
1. Conceptually I never really think about `HEAD` as a ref; “what
branch/commit am I on” is what I care about. It’s the only builtin
symref that I know of (or ref or symref depending on...). Not a usual
ref at all.
2. I use git-bisect(1) to find a commit given a criteria. Ones I have
it I note the commit. I don’t care that refs are used to store the
bisect state while a session is active.
According to gitglossary(7) these are currently the only per-worktree
refs. I do not know if you are allowed to use the `refs/worktree/`
hierarchy to create refs beyond that.
>
> It might of course also be surprising if you do that from the main
> working tree. But I think there's an argument to be made that it's at
> least _less_ surprising.
>
>> >> @@ -95,7 +96,7 @@ KNOWN LIMITATIONS
>> >>
>> >> The ref format migration has several known limitations in its current form:
>> >>
>> >> -* It is not possible to migrate repositories that have worktrees.
>> >> +* Migration must be run from the main worktree.
>> >>
>> >
>> > I'd drop this bullet point entirely, as I don't really see this as a
>> > limitation anymore.
>>
>> I agree that such a limitation should be lifted, but if we have to
>> say "you must do it this way, not that way", that is still a
>> limitation ;-).
>
> So with the above reasoning I'm not sure I'd call this a limitation.
> It's rather a mechanism to protect users from unexpected consequences.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-29 10:10 ` Patrick Steinhardt
2025-10-29 11:33 ` Kristoffer Haugsbakk
@ 2025-10-29 14:47 ` Junio C Hamano
1 sibling, 0 replies; 8+ messages in thread
From: Junio C Hamano @ 2025-10-29 14:47 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: Sam Bostock via GitGitGadget, git, Sam Bostock
Patrick Steinhardt <ps@pks.im> writes:
> The problem we have here is backwards compatibility. Right now we assume
> that `extensions.refStorage` applies to all worktrees, so if we wanted
> to change it like you propose then we'd have to introduce a backwards
> incompatible change.
That settles it. If we have long declared that a set of worktrees
attached to a repository share the same backend, then we do not have
to worry about overlaying refs stored in a different backend on top
of the base set of refs at all. That simplifies things a lot, I
would imagine.
> So with the above reasoning I'm not sure I'd call this a limitation.
> It's rather a mechanism to protect users from unexpected consequences.
The need for that mechanism would imply that it may not be clear to
the users that worktrees of the same repository must use the same
ref backend. Some education is needed, and erroring this operation
out may be one of the ways to give that, perhaps.
Thanks.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-29 11:33 ` Kristoffer Haugsbakk
@ 2025-10-29 16:22 ` Ben Knoble
2025-10-30 6:37 ` Patrick Steinhardt
1 sibling, 0 replies; 8+ messages in thread
From: Ben Knoble @ 2025-10-29 16:22 UTC (permalink / raw)
To: Kristoffer Haugsbakk
Cc: Patrick Steinhardt, Junio C Hamano, Josh Soref, git, Sam Bostock
> Le 29 oct. 2025 à 07:37, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> a écrit :
>
> On Wed, Oct 29, 2025, at 11:10, Patrick Steinhardt wrote:
>>> On Tue, Oct 28, 2025 at 09:00:43AM -0700, Junio C Hamano wrote:
>>> Patrick Steinhardt <ps@pks.im> writes:
[snip]
>>> * If "you must do so from the primary worktree and we convert all
>>> the worktrees attached to the same repository" is the only mode
>>> of operation we support (which by the way I have no problem
>>> with---the first bullet point above was asking question, not
>>> suggesting change of design), then would it be easier for the
>>> user to use if the command noticed that it is not in the primary
>>> worktree and switched to it for the user, instead of complaining
>>> and failing?
>>
>> I'm not sure. The question is whether the user recognizes that migrating
>> references in the worktree would also migrate references in the main
>> repository. It might be surprising behaviour if we did that without
>> asking.
>
> On the contrary, as a user I think it mattering what worktree I run this
> command from sounds very weird. (But again I can tolerate it requiring
> me to run it from the main worktree if there are technical difficulties/
> limitations. But using different backends for different
> worktrees is very weird, again.)
[snip]
The fewer concepts we ask a user to manage at a time, likely the better. In this case, “migrate the refs” should probably just work. While things are experimental, rough edges are more tolerable of course, but as we are lifting limitations towards making things official I think polishing such edges is a good idea.
In sum, it can be done later, but I think automatically changing the process directory to the main worktree and carrying on is fine. The curious folks would even see that under the TRACE output ;)
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] refs: support migration with worktrees
2025-10-29 11:33 ` Kristoffer Haugsbakk
2025-10-29 16:22 ` Ben Knoble
@ 2025-10-30 6:37 ` Patrick Steinhardt
1 sibling, 0 replies; 8+ messages in thread
From: Patrick Steinhardt @ 2025-10-30 6:37 UTC (permalink / raw)
To: Kristoffer Haugsbakk; +Cc: Junio C Hamano, Josh Soref, git, Sam Bostock
On Wed, Oct 29, 2025 at 12:33:15PM +0100, Kristoffer Haugsbakk wrote:
> On Wed, Oct 29, 2025, at 11:10, Patrick Steinhardt wrote:
> > On Tue, Oct 28, 2025 at 09:00:43AM -0700, Junio C Hamano wrote:
> >> Patrick Steinhardt <ps@pks.im> writes:
> >>
> >> >> `migrate`::
> >> >> - Migrate ref store between different formats.
> >> >> + Migrate ref store between different formats. Supports repositories
> >> >> + with worktrees; migration must be run from the main worktree.
> >> >
> >> > It feels a bit weird to single our worktrees specifically. We don't say
> >> > that the tool supports bare and non-bare repositories, either, so the
> >> > only reason why we'd have the note about worktrees is historic legacy.
> >> > How about this instead:
> >> >
> >> > Migrate ref storage between different formats. Must be run from the
> >> > main worktree in case the repository uses worktrees.
> >>
> >> Two thoughts.
> >>
> >> * Would it be unacceptable if the primary repository and refstore
> >> uses reftable backend, and a newly attached worktree to the
> >> repository uses ref-files only for its per-worktree refs? If we
> >> should allow it, then "if the ref store you are migrating is in a
> >> repository with multiple worktrees, you must migrate from the
> >> primary and migrate _all_ ref store for all worktrees at once,
> >> into the same backend", which the design of this patch seems to
> >> aim at, would contradict with it, no?
> >
> > The problem we have here is backwards compatibility. Right now we assume
> > that `extensions.refStorage` applies to all worktrees, so if we wanted
> > to change it like you propose then we'd have to introduce a backwards
> > incompatible change.
>
> I don’t understand the motivation or use case for supporting different
> backends for different worktrees. But Junio would have to explain that.
>
> Maybe the motivation is this weird (from a user’s perspective) limi-
> tation that you have to run a command from the main worktree? Okay,
> that’s strange but you get the error and switch to wherever the main
> worktree is (that the error message hopefully helpfully provides you
> with) and run the command there. Then you forget that weird thing five
> minutes later since this was a one-off command.
>
> >
> > I agree though that it would've been great if we would have said from
> > the beginning that the worktree-specific configuration is allowed to
> > override the ref storage format for a worktree. If so, we could easily
> > convert any of the worktrees (including the main one) by without having
> > any impact on all the other worktrees.
>
> As a user I don’t understand why that is a great thing to have.
I am commenting more from the developer side here. In the best case the
user wouldn't ever care what ref storage format they use. We simply pick
the best format available and the user lives happily ever after.
But from a developer standpoint it matters. If we had per-worktree ref
formats we would for example be able to make the ref migration code a
lot more robust, as we could now migrate worktrees one by one. In the
current situation we basically have to migrate all worktrees at once,
and that significantly increases the risk of the migration going wrong
at any point in time.
> >
> > But we do not live in such a world right now, and getting there would
> > require some significant reworking of how we handle per-worktree
> > references. Unfortunate, but I also don't think there's a strong enough
> > reason to change this.
> >
> >> * If "you must do so from the primary worktree and we convert all
> >> the worktrees attached to the same repository" is the only mode
> >> of operation we support (which by the way I have no problem
> >> with---the first bullet point above was asking question, not
> >> suggesting change of design), then would it be easier for the
> >> user to use if the command noticed that it is not in the primary
> >> worktree and switched to it for the user, instead of complaining
> >> and failing?
> >
> > I'm not sure. The question is whether the user recognizes that migrating
> > references in the worktree would also migrate references in the main
> > repository. It might be surprising behaviour if we did that without
> > asking.
>
> On the contrary, as a user I think it mattering what worktree I run this
> command from sounds very weird. (But again I can tolerate it requiring
> me to run it from the main worktree if there are technical difficulties/
> limitations. But using different backends for different
> worktrees is very weird, again.)
>
> If I run `git gc` I don’t want it to do different things based on what
> worktree I am. I want to operate on the repository, and the repository
> is the same no matter what worktree I am in. The same principle applies
> to this command in my mind.
It does though :) Only very slightly so, but for example maintenance of
references is dependent on the worktree you are in. We don't maintain
references from other worktrees. So it's not really a new thing that I'm
proposing here.
In any case, I'm happy to change my stance if the majority of folks
thinks that migrating the whole repository from secondary worktrees is
fine. I mostly wanted to avoid that operations that the user perform
have a wider blast radius than they understood, but if everyone agrees
that this is a non-issue then I don't mind much. It's only going to make
the implementation simpler.
Patrick
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2025-10-30 6:37 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-10-27 18:26 [PATCH] refs: support migration with worktrees Sam Bostock via GitGitGadget
2025-10-28 7:28 ` Patrick Steinhardt
2025-10-28 16:00 ` Junio C Hamano
2025-10-29 10:10 ` Patrick Steinhardt
2025-10-29 11:33 ` Kristoffer Haugsbakk
2025-10-29 16:22 ` Ben Knoble
2025-10-30 6:37 ` Patrick Steinhardt
2025-10-29 14:47 ` Junio C Hamano
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).