Skip to content

Commit f39a946

Browse files
committed
Support wholesale directory renames in fast-import
Some source material (e.g. Subversion dump files) perform directory renames without telling us exactly which files in that subdirectory were moved. This makes it hard for a frontend to convert such data formats to a fast-import stream, as all the frontend has on hand is "Rename a/ to b/" with no details about what files are in a/, unless the frontend also kept track of all files. The new 'R' subcommand within a commit allows the frontend to rename either a file or an entire subdirectory, without needing to know the object's SHA-1 or the specific files contained within it. The rename is performed as efficiently as possible internally, making it cheaper than a 'D'/'M' pair for a file rename. Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
1 parent 11a2640 commit f39a946

File tree

3 files changed

+168
-19
lines changed

3 files changed

+168
-19
lines changed

Documentation/git-fast-import.txt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ change to the project.
302302
data
303303
('from' SP <committish> LF)?
304304
('merge' SP <committish> LF)?
305-
(filemodify | filedelete | filedeleteall)*
305+
(filemodify | filedelete | filerename | filedeleteall)*
306306
LF
307307
....
308308

@@ -325,11 +325,13 @@ commit message use a 0 length data. Commit messages are free-form
325325
and are not interpreted by Git. Currently they must be encoded in
326326
UTF-8, as fast-import does not permit other encodings to be specified.
327327

328-
Zero or more `filemodify`, `filedelete` and `filedeleteall` commands
328+
Zero or more `filemodify`, `filedelete`, `filename` and
329+
`filedeleteall` commands
329330
may be included to update the contents of the branch prior to
330331
creating the commit. These commands may be supplied in any order.
331332
However it is recommended that a `filedeleteall` command preceed
332-
all `filemodify` commands in the same commit, as `filedeleteall`
333+
all `filemodify` and `filerename` commands in the same commit, as
334+
`filedeleteall`
333335
wipes the branch clean (see below).
334336

335337
`author`
@@ -495,6 +497,26 @@ here `<path>` is the complete path of the file or subdirectory to
495497
be removed from the branch.
496498
See `filemodify` above for a detailed description of `<path>`.
497499

500+
`filerename`
501+
^^^^^^^^^^^^
502+
Renames an existing file or subdirectory to a different location
503+
within the branch. The existing file or directory must exist. If
504+
the destination exists it will be replaced by the source directory.
505+
506+
....
507+
'R' SP <path> SP <path> LF
508+
....
509+
510+
here the first `<path>` is the source location and the second
511+
`<path>` is the destination. See `filemodify` above for a detailed
512+
description of what `<path>` may look like. To use a source path
513+
that contains SP the path must be quoted.
514+
515+
A `filerename` command takes effect immediately. Once the source
516+
location has been renamed to the destination any future commands
517+
applied to the source location will create new files there and not
518+
impact the destination of the rename.
519+
498520
`filedeleteall`
499521
^^^^^^^^^^^^^^^
500522
Included in a `commit` command to remove all files (and also all

fast-import.c

Lines changed: 75 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ Format of STDIN stream:
2626
lf;
2727
commit_msg ::= data;
2828
29-
file_change ::= file_clr | file_del | file_obm | file_inm;
29+
file_change ::= file_clr | file_del | file_rnm | file_obm | file_inm;
3030
file_clr ::= 'deleteall' lf;
3131
file_del ::= 'D' sp path_str lf;
32+
file_rnm ::= 'R' sp path_str sp path_str lf;
3233
file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf;
3334
file_inm ::= 'M' sp mode sp 'inline' sp path_str lf
3435
data;
@@ -1154,7 +1155,8 @@ static int tree_content_set(
11541155
struct tree_entry *root,
11551156
const char *p,
11561157
const unsigned char *sha1,
1157-
const uint16_t mode)
1158+
const uint16_t mode,
1159+
struct tree_content *subtree)
11581160
{
11591161
struct tree_content *t = root->tree;
11601162
const char *slash1;
@@ -1168,20 +1170,22 @@ static int tree_content_set(
11681170
n = strlen(p);
11691171
if (!n)
11701172
die("Empty path component found in input");
1173+
if (!slash1 && !S_ISDIR(mode) && subtree)
1174+
die("Non-directories cannot have subtrees");
11711175

11721176
for (i = 0; i < t->entry_count; i++) {
11731177
e = t->entries[i];
11741178
if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
11751179
if (!slash1) {
1176-
if (e->versions[1].mode == mode
1180+
if (!S_ISDIR(mode)
1181+
&& e->versions[1].mode == mode
11771182
&& !hashcmp(e->versions[1].sha1, sha1))
11781183
return 0;
11791184
e->versions[1].mode = mode;
11801185
hashcpy(e->versions[1].sha1, sha1);
1181-
if (e->tree) {
1186+
if (e->tree)
11821187
release_tree_content_recursive(e->tree);
1183-
e->tree = NULL;
1184-
}
1188+
e->tree = subtree;
11851189
hashclr(root->versions[1].sha1);
11861190
return 1;
11871191
}
@@ -1191,7 +1195,7 @@ static int tree_content_set(
11911195
}
11921196
if (!e->tree)
11931197
load_tree(e);
1194-
if (tree_content_set(e, slash1 + 1, sha1, mode)) {
1198+
if (tree_content_set(e, slash1 + 1, sha1, mode, subtree)) {
11951199
hashclr(root->versions[1].sha1);
11961200
return 1;
11971201
}
@@ -1209,17 +1213,20 @@ static int tree_content_set(
12091213
if (slash1) {
12101214
e->tree = new_tree_content(8);
12111215
e->versions[1].mode = S_IFDIR;
1212-
tree_content_set(e, slash1 + 1, sha1, mode);
1216+
tree_content_set(e, slash1 + 1, sha1, mode, subtree);
12131217
} else {
1214-
e->tree = NULL;
1218+
e->tree = subtree;
12151219
e->versions[1].mode = mode;
12161220
hashcpy(e->versions[1].sha1, sha1);
12171221
}
12181222
hashclr(root->versions[1].sha1);
12191223
return 1;
12201224
}
12211225

1222-
static int tree_content_remove(struct tree_entry *root, const char *p)
1226+
static int tree_content_remove(
1227+
struct tree_entry *root,
1228+
const char *p,
1229+
struct tree_entry *backup_leaf)
12231230
{
12241231
struct tree_content *t = root->tree;
12251232
const char *slash1;
@@ -1239,13 +1246,14 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
12391246
goto del_entry;
12401247
if (!e->tree)
12411248
load_tree(e);
1242-
if (tree_content_remove(e, slash1 + 1)) {
1249+
if (tree_content_remove(e, slash1 + 1, backup_leaf)) {
12431250
for (n = 0; n < e->tree->entry_count; n++) {
12441251
if (e->tree->entries[n]->versions[1].mode) {
12451252
hashclr(root->versions[1].sha1);
12461253
return 1;
12471254
}
12481255
}
1256+
backup_leaf = NULL;
12491257
goto del_entry;
12501258
}
12511259
return 0;
@@ -1254,10 +1262,11 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
12541262
return 0;
12551263

12561264
del_entry:
1257-
if (e->tree) {
1265+
if (backup_leaf)
1266+
memcpy(backup_leaf, e, sizeof(*backup_leaf));
1267+
else if (e->tree)
12581268
release_tree_content_recursive(e->tree);
1259-
e->tree = NULL;
1260-
}
1269+
e->tree = NULL;
12611270
e->versions[1].mode = 0;
12621271
hashclr(e->versions[1].sha1);
12631272
hashclr(root->versions[1].sha1);
@@ -1629,7 +1638,7 @@ static void file_change_m(struct branch *b)
16291638
typename(type), command_buf.buf);
16301639
}
16311640

1632-
tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode);
1641+
tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode, NULL);
16331642
free(p_uq);
16341643
}
16351644

@@ -1645,10 +1654,58 @@ static void file_change_d(struct branch *b)
16451654
die("Garbage after path in: %s", command_buf.buf);
16461655
p = p_uq;
16471656
}
1648-
tree_content_remove(&b->branch_tree, p);
1657+
tree_content_remove(&b->branch_tree, p, NULL);
16491658
free(p_uq);
16501659
}
16511660

1661+
static void file_change_r(struct branch *b)
1662+
{
1663+
const char *s, *d;
1664+
char *s_uq, *d_uq;
1665+
const char *endp;
1666+
struct tree_entry leaf;
1667+
1668+
s = command_buf.buf + 2;
1669+
s_uq = unquote_c_style(s, &endp);
1670+
if (s_uq) {
1671+
if (*endp != ' ')
1672+
die("Missing space after source: %s", command_buf.buf);
1673+
}
1674+
else {
1675+
endp = strchr(s, ' ');
1676+
if (!endp)
1677+
die("Missing space after source: %s", command_buf.buf);
1678+
s_uq = xmalloc(endp - s + 1);
1679+
memcpy(s_uq, s, endp - s);
1680+
s_uq[endp - s] = 0;
1681+
}
1682+
s = s_uq;
1683+
1684+
endp++;
1685+
if (!*endp)
1686+
die("Missing dest: %s", command_buf.buf);
1687+
1688+
d = endp;
1689+
d_uq = unquote_c_style(d, &endp);
1690+
if (d_uq) {
1691+
if (*endp)
1692+
die("Garbage after dest in: %s", command_buf.buf);
1693+
d = d_uq;
1694+
}
1695+
1696+
memset(&leaf, 0, sizeof(leaf));
1697+
tree_content_remove(&b->branch_tree, s, &leaf);
1698+
if (!leaf.versions[1].mode)
1699+
die("Path %s not in branch", s);
1700+
tree_content_set(&b->branch_tree, d,
1701+
leaf.versions[1].sha1,
1702+
leaf.versions[1].mode,
1703+
leaf.tree);
1704+
1705+
free(s_uq);
1706+
free(d_uq);
1707+
}
1708+
16521709
static void file_change_deleteall(struct branch *b)
16531710
{
16541711
release_tree_content_recursive(b->branch_tree.tree);
@@ -1816,6 +1873,8 @@ static void cmd_new_commit(void)
18161873
file_change_m(b);
18171874
else if (!prefixcmp(command_buf.buf, "D "))
18181875
file_change_d(b);
1876+
else if (!prefixcmp(command_buf.buf, "R "))
1877+
file_change_r(b);
18191878
else if (!strcmp("deleteall", command_buf.buf))
18201879
file_change_deleteall(b);
18211880
else

t/t9300-fast-import.sh

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,4 +580,72 @@ test_expect_success \
580580
git diff --raw L^ L >output &&
581581
git diff expect output'
582582

583+
###
584+
### series M
585+
###
586+
587+
test_tick
588+
cat >input <<INPUT_END
589+
commit refs/heads/M1
590+
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
591+
data <<COMMIT
592+
file rename
593+
COMMIT
594+
595+
from refs/heads/branch^0
596+
R file2/newf file2/n.e.w.f
597+
598+
INPUT_END
599+
600+
cat >expect <<EOF
601+
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 file2/newf file2/n.e.w.f
602+
EOF
603+
test_expect_success \
604+
'M: rename file in same subdirectory' \
605+
'git-fast-import <input &&
606+
git diff-tree -M -r M1^ M1 >actual &&
607+
compare_diff_raw expect actual'
608+
609+
cat >input <<INPUT_END
610+
commit refs/heads/M2
611+
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
612+
data <<COMMIT
613+
file rename
614+
COMMIT
615+
616+
from refs/heads/branch^0
617+
R file2/newf i/am/new/to/you
618+
619+
INPUT_END
620+
621+
cat >expect <<EOF
622+
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 file2/newf i/am/new/to/you
623+
EOF
624+
test_expect_success \
625+
'M: rename file to new subdirectory' \
626+
'git-fast-import <input &&
627+
git diff-tree -M -r M2^ M2 >actual &&
628+
compare_diff_raw expect actual'
629+
630+
cat >input <<INPUT_END
631+
commit refs/heads/M3
632+
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
633+
data <<COMMIT
634+
file rename
635+
COMMIT
636+
637+
from refs/heads/M2^0
638+
R i other/sub
639+
640+
INPUT_END
641+
642+
cat >expect <<EOF
643+
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 i/am/new/to/you other/sub/am/new/to/you
644+
EOF
645+
test_expect_success \
646+
'M: rename subdirectory to new subdirectory' \
647+
'git-fast-import <input &&
648+
git diff-tree -M -r M3^ M3 >actual &&
649+
compare_diff_raw expect actual'
650+
583651
test_done

0 commit comments

Comments
 (0)