Coverage Report

Created: 2024-09-16 06:10

/src/git/builtin/mv.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * "git mv" builtin command
3
 *
4
 * Copyright (C) 2006 Johannes Schindelin
5
 */
6
7
#include "builtin.h"
8
#include "abspath.h"
9
#include "advice.h"
10
#include "config.h"
11
#include "environment.h"
12
#include "gettext.h"
13
#include "name-hash.h"
14
#include "object-file.h"
15
#include "pathspec.h"
16
#include "lockfile.h"
17
#include "dir.h"
18
#include "string-list.h"
19
#include "parse-options.h"
20
#include "read-cache-ll.h"
21
#include "repository.h"
22
#include "setup.h"
23
#include "strvec.h"
24
#include "submodule.h"
25
#include "entry.h"
26
27
static const char * const builtin_mv_usage[] = {
28
  N_("git mv [<options>] <source>... <destination>"),
29
  NULL
30
};
31
32
enum update_mode {
33
  WORKING_DIRECTORY = (1 << 1),
34
  INDEX = (1 << 2),
35
  SPARSE = (1 << 3),
36
  SKIP_WORKTREE_DIR = (1 << 4),
37
};
38
39
0
#define DUP_BASENAME 1
40
0
#define KEEP_TRAILING_SLASH 2
41
42
static void internal_prefix_pathspec(struct strvec *out,
43
             const char *prefix,
44
             const char **pathspec,
45
             int count, unsigned flags)
46
0
{
47
0
  int prefixlen = prefix ? strlen(prefix) : 0;
48
49
  /* Create an intermediate copy of the pathspec based on the flags */
50
0
  for (int i = 0; i < count; i++) {
51
0
    size_t length = strlen(pathspec[i]);
52
0
    size_t to_copy = length;
53
0
    const char *maybe_basename;
54
0
    char *trimmed, *prefixed_path;
55
56
0
    while (!(flags & KEEP_TRAILING_SLASH) &&
57
0
           to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1]))
58
0
      to_copy--;
59
60
0
    trimmed = xmemdupz(pathspec[i], to_copy);
61
0
    maybe_basename = (flags & DUP_BASENAME) ? basename(trimmed) : trimmed;
62
0
    prefixed_path = prefix_path(prefix, prefixlen, maybe_basename);
63
0
    strvec_push(out, prefixed_path);
64
65
0
    free(prefixed_path);
66
0
    free(trimmed);
67
0
  }
68
0
}
69
70
static char *add_slash(const char *path)
71
0
{
72
0
  size_t len = strlen(path);
73
0
  if (len && path[len - 1] != '/') {
74
0
    char *with_slash = xmalloc(st_add(len, 2));
75
0
    memcpy(with_slash, path, len);
76
0
    with_slash[len++] = '/';
77
0
    with_slash[len] = 0;
78
0
    return with_slash;
79
0
  }
80
0
  return xstrdup(path);
81
0
}
82
83
0
#define SUBMODULE_WITH_GITDIR ((const char *)1)
84
85
static const char *submodule_gitfile_path(const char *src, int first)
86
0
{
87
0
  struct strbuf submodule_dotgit = STRBUF_INIT;
88
0
  const char *path;
89
90
0
  if (!S_ISGITLINK(the_repository->index->cache[first]->ce_mode))
91
0
    die(_("Directory %s is in index and no submodule?"), src);
92
0
  if (!is_staging_gitmodules_ok(the_repository->index))
93
0
    die(_("Please stage your changes to .gitmodules or stash them to proceed"));
94
95
0
  strbuf_addf(&submodule_dotgit, "%s/.git", src);
96
97
0
  path = read_gitfile(submodule_dotgit.buf);
98
0
  strbuf_release(&submodule_dotgit);
99
0
  if (path)
100
0
    return path;
101
0
  return SUBMODULE_WITH_GITDIR;
102
0
}
103
104
static int index_range_of_same_dir(const char *src, int length,
105
           int *first_p, int *last_p)
106
0
{
107
0
  char *src_w_slash = add_slash(src);
108
0
  int first, last, len_w_slash = length + 1;
109
110
0
  first = index_name_pos(the_repository->index, src_w_slash, len_w_slash);
111
0
  if (first >= 0)
112
0
    die(_("%.*s is in index"), len_w_slash, src_w_slash);
113
114
0
  first = -1 - first;
115
0
  for (last = first; last < the_repository->index->cache_nr; last++) {
116
0
    const char *path = the_repository->index->cache[last]->name;
117
0
    if (strncmp(path, src_w_slash, len_w_slash))
118
0
      break;
119
0
  }
120
121
0
  free(src_w_slash);
122
0
  *first_p = first;
123
0
  *last_p = last;
124
0
  return last - first;
125
0
}
126
127
/*
128
 * Given the path of a directory that does not exist on-disk, check whether the
129
 * directory contains any entries in the index with the SKIP_WORKTREE flag
130
 * enabled.
131
 * Return 1 if such index entries exist.
132
 * Return 0 otherwise.
133
 */
134
static int empty_dir_has_sparse_contents(const char *name)
135
0
{
136
0
  int ret = 0;
137
0
  char *with_slash = add_slash(name);
138
0
  int length = strlen(with_slash);
139
140
0
  int pos = index_name_pos(the_repository->index, with_slash, length);
141
0
  const struct cache_entry *ce;
142
143
0
  if (pos < 0) {
144
0
    pos = -pos - 1;
145
0
    if (pos >= the_repository->index->cache_nr)
146
0
      goto free_return;
147
0
    ce = the_repository->index->cache[pos];
148
0
    if (strncmp(with_slash, ce->name, length))
149
0
      goto free_return;
150
0
    if (ce_skip_worktree(ce))
151
0
      ret = 1;
152
0
  }
153
154
0
free_return:
155
0
  free(with_slash);
156
0
  return ret;
157
0
}
158
159
static void remove_empty_src_dirs(const char **src_dir, size_t src_dir_nr)
160
0
{
161
0
  size_t i;
162
0
  struct strbuf a_src_dir = STRBUF_INIT;
163
164
0
  for (i = 0; i < src_dir_nr; i++) {
165
0
    int dummy;
166
0
    strbuf_addstr(&a_src_dir, src_dir[i]);
167
    /*
168
     * if entries under a_src_dir are all moved away,
169
     * recursively remove a_src_dir to cleanup
170
     */
171
0
    if (index_range_of_same_dir(a_src_dir.buf, a_src_dir.len,
172
0
              &dummy, &dummy) < 1) {
173
0
      remove_dir_recursively(&a_src_dir, 0);
174
0
    }
175
0
    strbuf_reset(&a_src_dir);
176
0
  }
177
178
0
  strbuf_release(&a_src_dir);
179
0
}
180
181
int cmd_mv(int argc, const char **argv, const char *prefix)
182
0
{
183
0
  int i, flags, gitmodules_modified = 0;
184
0
  int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0;
185
0
  struct option builtin_mv_options[] = {
186
0
    OPT__VERBOSE(&verbose, N_("be verbose")),
187
0
    OPT__DRY_RUN(&show_only, N_("dry run")),
188
0
    OPT__FORCE(&force, N_("force move/rename even if target exists"),
189
0
         PARSE_OPT_NOCOMPLETE),
190
0
    OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")),
191
0
    OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")),
192
0
    OPT_END(),
193
0
  };
194
0
  struct strvec sources = STRVEC_INIT;
195
0
  struct strvec dest_paths = STRVEC_INIT;
196
0
  struct strvec destinations = STRVEC_INIT;
197
0
  struct strvec submodule_gitfiles_to_free = STRVEC_INIT;
198
0
  const char **submodule_gitfiles;
199
0
  char *dst_w_slash = NULL;
200
0
  struct strvec src_dir = STRVEC_INIT;
201
0
  enum update_mode *modes, dst_mode = 0;
202
0
  struct stat st, dest_st;
203
0
  struct string_list src_for_dst = STRING_LIST_INIT_DUP;
204
0
  struct lock_file lock_file = LOCK_INIT;
205
0
  struct cache_entry *ce;
206
0
  struct string_list only_match_skip_worktree = STRING_LIST_INIT_DUP;
207
0
  struct string_list dirty_paths = STRING_LIST_INIT_DUP;
208
0
  int ret;
209
210
0
  git_config(git_default_config, NULL);
211
212
0
  argc = parse_options(argc, argv, prefix, builtin_mv_options,
213
0
           builtin_mv_usage, 0);
214
0
  if (--argc < 1)
215
0
    usage_with_options(builtin_mv_usage, builtin_mv_options);
216
217
0
  repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
218
0
  if (repo_read_index(the_repository) < 0)
219
0
    die(_("index file corrupt"));
220
221
0
  internal_prefix_pathspec(&sources, prefix, argv, argc, 0);
222
0
  CALLOC_ARRAY(modes, argc);
223
224
  /*
225
   * Keep trailing slash, needed to let
226
   * "git mv file no-such-dir/" error out, except in the case
227
   * "git mv directory no-such-dir/".
228
   */
229
0
  flags = KEEP_TRAILING_SLASH;
230
0
  if (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1]))
231
0
    flags = 0;
232
0
  internal_prefix_pathspec(&dest_paths, prefix, argv + argc, 1, flags);
233
0
  dst_w_slash = add_slash(dest_paths.v[0]);
234
0
  submodule_gitfiles = xcalloc(argc, sizeof(char *));
235
236
0
  if (dest_paths.v[0][0] == '\0')
237
    /* special case: "." was normalized to "" */
238
0
    internal_prefix_pathspec(&destinations, dest_paths.v[0], argv, argc, DUP_BASENAME);
239
0
  else if (!lstat(dest_paths.v[0], &st) && S_ISDIR(st.st_mode)) {
240
0
    internal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);
241
0
  } else if (!path_in_sparse_checkout(dst_w_slash, the_repository->index) &&
242
0
       empty_dir_has_sparse_contents(dst_w_slash)) {
243
0
    internal_prefix_pathspec(&destinations, dst_w_slash, argv, argc, DUP_BASENAME);
244
0
    dst_mode = SKIP_WORKTREE_DIR;
245
0
  } else if (argc != 1) {
246
0
    die(_("destination '%s' is not a directory"), dest_paths.v[0]);
247
0
  } else {
248
0
    strvec_pushv(&destinations, dest_paths.v);
249
250
    /*
251
     * <destination> is a file outside of sparse-checkout
252
     * cone. Insist on cone mode here for backward
253
     * compatibility. We don't want dst_mode to be assigned
254
     * for a file when the repo is using no-cone mode (which
255
     * is deprecated at this point) sparse-checkout. As
256
     * SPARSE here is only considering cone-mode situation.
257
     */
258
0
    if (!path_in_cone_mode_sparse_checkout(destinations.v[0], the_repository->index))
259
0
      dst_mode = SPARSE;
260
0
  }
261
262
  /* Checking */
263
0
  for (i = 0; i < argc; i++) {
264
0
    const char *src = sources.v[i], *dst = destinations.v[i];
265
0
    int length;
266
0
    const char *bad = NULL;
267
0
    int skip_sparse = 0;
268
269
0
    if (show_only)
270
0
      printf(_("Checking rename of '%s' to '%s'\n"), src, dst);
271
272
0
    length = strlen(src);
273
0
    if (lstat(src, &st) < 0) {
274
0
      int pos;
275
0
      const struct cache_entry *ce;
276
277
0
      pos = index_name_pos(the_repository->index, src, length);
278
0
      if (pos < 0) {
279
0
        char *src_w_slash = add_slash(src);
280
0
        if (!path_in_sparse_checkout(src_w_slash, the_repository->index) &&
281
0
            empty_dir_has_sparse_contents(src)) {
282
0
          free(src_w_slash);
283
0
          modes[i] |= SKIP_WORKTREE_DIR;
284
0
          goto dir_check;
285
0
        }
286
0
        free(src_w_slash);
287
        /* only error if existence is expected. */
288
0
        if (!(modes[i] & SPARSE))
289
0
          bad = _("bad source");
290
0
        goto act_on_entry;
291
0
      }
292
0
      ce = the_repository->index->cache[pos];
293
0
      if (!ce_skip_worktree(ce)) {
294
0
        bad = _("bad source");
295
0
        goto act_on_entry;
296
0
      }
297
0
      if (!ignore_sparse) {
298
0
        string_list_append(&only_match_skip_worktree, src);
299
0
        goto act_on_entry;
300
0
      }
301
      /* Check if dst exists in index */
302
0
      if (index_name_pos(the_repository->index, dst, strlen(dst)) < 0) {
303
0
        modes[i] |= SPARSE;
304
0
        goto act_on_entry;
305
0
      }
306
0
      if (!force) {
307
0
        bad = _("destination exists");
308
0
        goto act_on_entry;
309
0
      }
310
0
      modes[i] |= SPARSE;
311
0
      goto act_on_entry;
312
0
    }
313
0
    if (!strncmp(src, dst, length) &&
314
0
        (dst[length] == 0 || dst[length] == '/')) {
315
0
      bad = _("can not move directory into itself");
316
0
      goto act_on_entry;
317
0
    }
318
0
    if (S_ISDIR(st.st_mode)
319
0
        && lstat(dst, &dest_st) == 0) {
320
0
      bad = _("destination already exists");
321
0
      goto act_on_entry;
322
0
    }
323
324
0
dir_check:
325
0
    if (S_ISDIR(st.st_mode)) {
326
0
      char *dst_with_slash;
327
0
      size_t dst_with_slash_len;
328
0
      int j, n;
329
0
      int first = index_name_pos(the_repository->index, src, length), last;
330
331
0
      if (first >= 0) {
332
0
        const char *path = submodule_gitfile_path(src, first);
333
0
        if (path != SUBMODULE_WITH_GITDIR)
334
0
          path = strvec_push(&submodule_gitfiles_to_free, path);
335
0
        submodule_gitfiles[i] = path;
336
0
        goto act_on_entry;
337
0
      } else if (index_range_of_same_dir(src, length,
338
0
                 &first, &last) < 1) {
339
0
        bad = _("source directory is empty");
340
0
        goto act_on_entry;
341
0
      }
342
343
      /* last - first >= 1 */
344
0
      modes[i] |= WORKING_DIRECTORY;
345
346
0
      strvec_push(&src_dir, src);
347
348
0
      n = argc + last - first;
349
0
      REALLOC_ARRAY(modes, n);
350
0
      REALLOC_ARRAY(submodule_gitfiles, n);
351
352
0
      dst_with_slash = add_slash(dst);
353
0
      dst_with_slash_len = strlen(dst_with_slash);
354
355
0
      for (j = 0; j < last - first; j++) {
356
0
        const struct cache_entry *ce = the_repository->index->cache[first + j];
357
0
        const char *path = ce->name;
358
0
        char *prefixed_path = prefix_path(dst_with_slash, dst_with_slash_len, path + length + 1);
359
360
0
        strvec_push(&sources, path);
361
0
        strvec_push(&destinations, prefixed_path);
362
363
0
        memset(modes + argc + j, 0, sizeof(enum update_mode));
364
0
        modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX;
365
0
        submodule_gitfiles[argc + j] = NULL;
366
367
0
        free(prefixed_path);
368
0
      }
369
370
0
      free(dst_with_slash);
371
0
      argc += last - first;
372
0
      goto act_on_entry;
373
0
    }
374
0
    if (!(ce = index_file_exists(the_repository->index, src, length, 0))) {
375
0
      bad = _("not under version control");
376
0
      goto act_on_entry;
377
0
    }
378
0
    if (ce_stage(ce)) {
379
0
      bad = _("conflicted");
380
0
      goto act_on_entry;
381
0
    }
382
0
    if (lstat(dst, &st) == 0 &&
383
0
        (!ignore_case || strcasecmp(src, dst))) {
384
0
      bad = _("destination exists");
385
0
      if (force) {
386
        /*
387
         * only files can overwrite each other:
388
         * check both source and destination
389
         */
390
0
        if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
391
0
          if (verbose)
392
0
            warning(_("overwriting '%s'"), dst);
393
0
          bad = NULL;
394
0
        } else
395
0
          bad = _("Cannot overwrite");
396
0
      }
397
0
      goto act_on_entry;
398
0
    }
399
0
    if (string_list_has_string(&src_for_dst, dst)) {
400
0
      bad = _("multiple sources for the same target");
401
0
      goto act_on_entry;
402
0
    }
403
0
    if (is_dir_sep(dst[strlen(dst) - 1])) {
404
0
      bad = _("destination directory does not exist");
405
0
      goto act_on_entry;
406
0
    }
407
408
0
    if (ignore_sparse &&
409
0
        (dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
410
0
        index_entry_exists(the_repository->index, dst, strlen(dst))) {
411
0
      bad = _("destination exists in the index");
412
0
      if (force) {
413
0
        if (verbose)
414
0
          warning(_("overwriting '%s'"), dst);
415
0
        bad = NULL;
416
0
      } else {
417
0
        goto act_on_entry;
418
0
      }
419
0
    }
420
    /*
421
     * We check if the paths are in the sparse-checkout
422
     * definition as a very final check, since that
423
     * allows us to point the user to the --sparse
424
     * option as a way to have a successful run.
425
     */
426
0
    if (!ignore_sparse &&
427
0
        !path_in_sparse_checkout(src, the_repository->index)) {
428
0
      string_list_append(&only_match_skip_worktree, src);
429
0
      skip_sparse = 1;
430
0
    }
431
0
    if (!ignore_sparse &&
432
0
        !path_in_sparse_checkout(dst, the_repository->index)) {
433
0
      string_list_append(&only_match_skip_worktree, dst);
434
0
      skip_sparse = 1;
435
0
    }
436
437
0
    if (skip_sparse)
438
0
      goto remove_entry;
439
440
0
    string_list_insert(&src_for_dst, dst);
441
442
0
act_on_entry:
443
0
    if (!bad)
444
0
      continue;
445
0
    if (!ignore_errors)
446
0
      die(_("%s, source=%s, destination=%s"),
447
0
           bad, src, dst);
448
0
remove_entry:
449
0
    if (--argc > 0) {
450
0
      int n = argc - i;
451
0
      strvec_remove(&sources, i);
452
0
      strvec_remove(&destinations, i);
453
0
      MOVE_ARRAY(modes + i, modes + i + 1, n);
454
0
      MOVE_ARRAY(submodule_gitfiles + i,
455
0
           submodule_gitfiles + i + 1, n);
456
0
      i--;
457
0
    }
458
0
  }
459
460
0
  if (only_match_skip_worktree.nr) {
461
0
    advise_on_updating_sparse_paths(&only_match_skip_worktree);
462
0
    if (!ignore_errors) {
463
0
      ret = 1;
464
0
      goto out;
465
0
    }
466
0
  }
467
468
0
  for (i = 0; i < argc; i++) {
469
0
    const char *src = sources.v[i], *dst = destinations.v[i];
470
0
    enum update_mode mode = modes[i];
471
0
    int pos;
472
0
    int sparse_and_dirty = 0;
473
0
    struct checkout state = CHECKOUT_INIT;
474
0
    state.istate = the_repository->index;
475
476
0
    if (force)
477
0
      state.force = 1;
478
0
    if (show_only || verbose)
479
0
      printf(_("Renaming %s to %s\n"), src, dst);
480
0
    if (show_only)
481
0
      continue;
482
0
    if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) &&
483
0
        !(dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
484
0
        rename(src, dst) < 0) {
485
0
      if (ignore_errors)
486
0
        continue;
487
0
      die_errno(_("renaming '%s' failed"), src);
488
0
    }
489
0
    if (submodule_gitfiles[i]) {
490
0
      if (!update_path_in_gitmodules(src, dst))
491
0
        gitmodules_modified = 1;
492
0
      if (submodule_gitfiles[i] != SUBMODULE_WITH_GITDIR)
493
0
        connect_work_tree_and_git_dir(dst,
494
0
                    submodule_gitfiles[i],
495
0
                    1);
496
0
    }
497
498
0
    if (mode & (WORKING_DIRECTORY | SKIP_WORKTREE_DIR))
499
0
      continue;
500
501
0
    pos = index_name_pos(the_repository->index, src, strlen(src));
502
0
    assert(pos >= 0);
503
0
    if (!(mode & SPARSE) && !lstat(src, &st))
504
0
      sparse_and_dirty = ie_modified(the_repository->index,
505
0
                   the_repository->index->cache[pos],
506
0
                   &st,
507
0
                   0);
508
0
    rename_index_entry_at(the_repository->index, pos, dst);
509
510
0
    if (ignore_sparse &&
511
0
        core_apply_sparse_checkout &&
512
0
        core_sparse_checkout_cone) {
513
      /*
514
       * NEEDSWORK: we are *not* paying attention to
515
       * "out-to-out" move (<source> is out-of-cone and
516
       * <destination> is out-of-cone) at this point. It
517
       * should be added in a future patch.
518
       */
519
0
      if ((mode & SPARSE) &&
520
0
          path_in_sparse_checkout(dst, the_repository->index)) {
521
        /* from out-of-cone to in-cone */
522
0
        int dst_pos = index_name_pos(the_repository->index, dst,
523
0
                   strlen(dst));
524
0
        struct cache_entry *dst_ce = the_repository->index->cache[dst_pos];
525
526
0
        dst_ce->ce_flags &= ~CE_SKIP_WORKTREE;
527
528
0
        if (checkout_entry(dst_ce, &state, NULL, NULL))
529
0
          die(_("cannot checkout %s"), dst_ce->name);
530
0
      } else if ((dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) &&
531
0
           !(mode & SPARSE) &&
532
0
           !path_in_sparse_checkout(dst, the_repository->index)) {
533
        /* from in-cone to out-of-cone */
534
0
        int dst_pos = index_name_pos(the_repository->index, dst,
535
0
                   strlen(dst));
536
0
        struct cache_entry *dst_ce = the_repository->index->cache[dst_pos];
537
538
        /*
539
         * if src is clean, it will suffice to remove it
540
         */
541
0
        if (!sparse_and_dirty) {
542
0
          dst_ce->ce_flags |= CE_SKIP_WORKTREE;
543
0
          unlink_or_warn(src);
544
0
        } else {
545
          /*
546
           * if src is dirty, move it to the
547
           * destination and create leading
548
           * dirs if necessary
549
           */
550
0
          char *dst_dup = xstrdup(dst);
551
0
          string_list_append(&dirty_paths, dst);
552
0
          safe_create_leading_directories(dst_dup);
553
0
          FREE_AND_NULL(dst_dup);
554
0
          rename(src, dst);
555
0
        }
556
0
      }
557
0
    }
558
0
  }
559
560
0
  remove_empty_src_dirs(src_dir.v, src_dir.nr);
561
562
0
  if (dirty_paths.nr)
563
0
    advise_on_moving_dirty_path(&dirty_paths);
564
565
0
  if (gitmodules_modified)
566
0
    stage_updated_gitmodules(the_repository->index);
567
568
0
  if (write_locked_index(the_repository->index, &lock_file,
569
0
             COMMIT_LOCK | SKIP_IF_UNCHANGED))
570
0
    die(_("Unable to write new index file"));
571
572
0
  ret = 0;
573
574
0
out:
575
0
  strvec_clear(&src_dir);
576
0
  free(dst_w_slash);
577
0
  string_list_clear(&src_for_dst, 0);
578
0
  string_list_clear(&dirty_paths, 0);
579
0
  string_list_clear(&only_match_skip_worktree, 0);
580
0
  strvec_clear(&sources);
581
0
  strvec_clear(&dest_paths);
582
0
  strvec_clear(&destinations);
583
0
  strvec_clear(&submodule_gitfiles_to_free);
584
0
  free(submodule_gitfiles);
585
0
  free(modes);
586
0
  return ret;
587
0
}