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>
This commit is contained in:
parent
11a264050f
commit
f39a946a1f
@ -302,7 +302,7 @@ change to the project.
|
|||||||
data
|
data
|
||||||
('from' SP <committish> LF)?
|
('from' SP <committish> LF)?
|
||||||
('merge' SP <committish> LF)?
|
('merge' SP <committish> LF)?
|
||||||
(filemodify | filedelete | filedeleteall)*
|
(filemodify | filedelete | filerename | filedeleteall)*
|
||||||
LF
|
LF
|
||||||
....
|
....
|
||||||
|
|
||||||
@ -325,11 +325,13 @@ commit message use a 0 length data. Commit messages are free-form
|
|||||||
and are not interpreted by Git. Currently they must be encoded in
|
and are not interpreted by Git. Currently they must be encoded in
|
||||||
UTF-8, as fast-import does not permit other encodings to be specified.
|
UTF-8, as fast-import does not permit other encodings to be specified.
|
||||||
|
|
||||||
Zero or more `filemodify`, `filedelete` and `filedeleteall` commands
|
Zero or more `filemodify`, `filedelete`, `filename` and
|
||||||
|
`filedeleteall` commands
|
||||||
may be included to update the contents of the branch prior to
|
may be included to update the contents of the branch prior to
|
||||||
creating the commit. These commands may be supplied in any order.
|
creating the commit. These commands may be supplied in any order.
|
||||||
However it is recommended that a `filedeleteall` command preceed
|
However it is recommended that a `filedeleteall` command preceed
|
||||||
all `filemodify` commands in the same commit, as `filedeleteall`
|
all `filemodify` and `filerename` commands in the same commit, as
|
||||||
|
`filedeleteall`
|
||||||
wipes the branch clean (see below).
|
wipes the branch clean (see below).
|
||||||
|
|
||||||
`author`
|
`author`
|
||||||
@ -495,6 +497,26 @@ here `<path>` is the complete path of the file or subdirectory to
|
|||||||
be removed from the branch.
|
be removed from the branch.
|
||||||
See `filemodify` above for a detailed description of `<path>`.
|
See `filemodify` above for a detailed description of `<path>`.
|
||||||
|
|
||||||
|
`filerename`
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
Renames an existing file or subdirectory to a different location
|
||||||
|
within the branch. The existing file or directory must exist. If
|
||||||
|
the destination exists it will be replaced by the source directory.
|
||||||
|
|
||||||
|
....
|
||||||
|
'R' SP <path> SP <path> LF
|
||||||
|
....
|
||||||
|
|
||||||
|
here the first `<path>` is the source location and the second
|
||||||
|
`<path>` is the destination. See `filemodify` above for a detailed
|
||||||
|
description of what `<path>` may look like. To use a source path
|
||||||
|
that contains SP the path must be quoted.
|
||||||
|
|
||||||
|
A `filerename` command takes effect immediately. Once the source
|
||||||
|
location has been renamed to the destination any future commands
|
||||||
|
applied to the source location will create new files there and not
|
||||||
|
impact the destination of the rename.
|
||||||
|
|
||||||
`filedeleteall`
|
`filedeleteall`
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
Included in a `commit` command to remove all files (and also all
|
Included in a `commit` command to remove all files (and also all
|
||||||
|
@ -26,9 +26,10 @@ Format of STDIN stream:
|
|||||||
lf;
|
lf;
|
||||||
commit_msg ::= data;
|
commit_msg ::= data;
|
||||||
|
|
||||||
file_change ::= file_clr | file_del | file_obm | file_inm;
|
file_change ::= file_clr | file_del | file_rnm | file_obm | file_inm;
|
||||||
file_clr ::= 'deleteall' lf;
|
file_clr ::= 'deleteall' lf;
|
||||||
file_del ::= 'D' sp path_str lf;
|
file_del ::= 'D' sp path_str lf;
|
||||||
|
file_rnm ::= 'R' sp path_str sp path_str lf;
|
||||||
file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf;
|
file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf;
|
||||||
file_inm ::= 'M' sp mode sp 'inline' sp path_str lf
|
file_inm ::= 'M' sp mode sp 'inline' sp path_str lf
|
||||||
data;
|
data;
|
||||||
@ -1154,7 +1155,8 @@ static int tree_content_set(
|
|||||||
struct tree_entry *root,
|
struct tree_entry *root,
|
||||||
const char *p,
|
const char *p,
|
||||||
const unsigned char *sha1,
|
const unsigned char *sha1,
|
||||||
const uint16_t mode)
|
const uint16_t mode,
|
||||||
|
struct tree_content *subtree)
|
||||||
{
|
{
|
||||||
struct tree_content *t = root->tree;
|
struct tree_content *t = root->tree;
|
||||||
const char *slash1;
|
const char *slash1;
|
||||||
@ -1168,20 +1170,22 @@ static int tree_content_set(
|
|||||||
n = strlen(p);
|
n = strlen(p);
|
||||||
if (!n)
|
if (!n)
|
||||||
die("Empty path component found in input");
|
die("Empty path component found in input");
|
||||||
|
if (!slash1 && !S_ISDIR(mode) && subtree)
|
||||||
|
die("Non-directories cannot have subtrees");
|
||||||
|
|
||||||
for (i = 0; i < t->entry_count; i++) {
|
for (i = 0; i < t->entry_count; i++) {
|
||||||
e = t->entries[i];
|
e = t->entries[i];
|
||||||
if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
|
if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
|
||||||
if (!slash1) {
|
if (!slash1) {
|
||||||
if (e->versions[1].mode == mode
|
if (!S_ISDIR(mode)
|
||||||
|
&& e->versions[1].mode == mode
|
||||||
&& !hashcmp(e->versions[1].sha1, sha1))
|
&& !hashcmp(e->versions[1].sha1, sha1))
|
||||||
return 0;
|
return 0;
|
||||||
e->versions[1].mode = mode;
|
e->versions[1].mode = mode;
|
||||||
hashcpy(e->versions[1].sha1, sha1);
|
hashcpy(e->versions[1].sha1, sha1);
|
||||||
if (e->tree) {
|
if (e->tree)
|
||||||
release_tree_content_recursive(e->tree);
|
release_tree_content_recursive(e->tree);
|
||||||
e->tree = NULL;
|
e->tree = subtree;
|
||||||
}
|
|
||||||
hashclr(root->versions[1].sha1);
|
hashclr(root->versions[1].sha1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -1191,7 +1195,7 @@ static int tree_content_set(
|
|||||||
}
|
}
|
||||||
if (!e->tree)
|
if (!e->tree)
|
||||||
load_tree(e);
|
load_tree(e);
|
||||||
if (tree_content_set(e, slash1 + 1, sha1, mode)) {
|
if (tree_content_set(e, slash1 + 1, sha1, mode, subtree)) {
|
||||||
hashclr(root->versions[1].sha1);
|
hashclr(root->versions[1].sha1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@ -1209,9 +1213,9 @@ static int tree_content_set(
|
|||||||
if (slash1) {
|
if (slash1) {
|
||||||
e->tree = new_tree_content(8);
|
e->tree = new_tree_content(8);
|
||||||
e->versions[1].mode = S_IFDIR;
|
e->versions[1].mode = S_IFDIR;
|
||||||
tree_content_set(e, slash1 + 1, sha1, mode);
|
tree_content_set(e, slash1 + 1, sha1, mode, subtree);
|
||||||
} else {
|
} else {
|
||||||
e->tree = NULL;
|
e->tree = subtree;
|
||||||
e->versions[1].mode = mode;
|
e->versions[1].mode = mode;
|
||||||
hashcpy(e->versions[1].sha1, sha1);
|
hashcpy(e->versions[1].sha1, sha1);
|
||||||
}
|
}
|
||||||
@ -1219,7 +1223,10 @@ static int tree_content_set(
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int tree_content_remove(struct tree_entry *root, const char *p)
|
static int tree_content_remove(
|
||||||
|
struct tree_entry *root,
|
||||||
|
const char *p,
|
||||||
|
struct tree_entry *backup_leaf)
|
||||||
{
|
{
|
||||||
struct tree_content *t = root->tree;
|
struct tree_content *t = root->tree;
|
||||||
const char *slash1;
|
const char *slash1;
|
||||||
@ -1239,13 +1246,14 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
|
|||||||
goto del_entry;
|
goto del_entry;
|
||||||
if (!e->tree)
|
if (!e->tree)
|
||||||
load_tree(e);
|
load_tree(e);
|
||||||
if (tree_content_remove(e, slash1 + 1)) {
|
if (tree_content_remove(e, slash1 + 1, backup_leaf)) {
|
||||||
for (n = 0; n < e->tree->entry_count; n++) {
|
for (n = 0; n < e->tree->entry_count; n++) {
|
||||||
if (e->tree->entries[n]->versions[1].mode) {
|
if (e->tree->entries[n]->versions[1].mode) {
|
||||||
hashclr(root->versions[1].sha1);
|
hashclr(root->versions[1].sha1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
backup_leaf = NULL;
|
||||||
goto del_entry;
|
goto del_entry;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@ -1254,10 +1262,11 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
|
|||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
del_entry:
|
del_entry:
|
||||||
if (e->tree) {
|
if (backup_leaf)
|
||||||
|
memcpy(backup_leaf, e, sizeof(*backup_leaf));
|
||||||
|
else if (e->tree)
|
||||||
release_tree_content_recursive(e->tree);
|
release_tree_content_recursive(e->tree);
|
||||||
e->tree = NULL;
|
e->tree = NULL;
|
||||||
}
|
|
||||||
e->versions[1].mode = 0;
|
e->versions[1].mode = 0;
|
||||||
hashclr(e->versions[1].sha1);
|
hashclr(e->versions[1].sha1);
|
||||||
hashclr(root->versions[1].sha1);
|
hashclr(root->versions[1].sha1);
|
||||||
@ -1629,7 +1638,7 @@ static void file_change_m(struct branch *b)
|
|||||||
typename(type), command_buf.buf);
|
typename(type), command_buf.buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode);
|
tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode, NULL);
|
||||||
free(p_uq);
|
free(p_uq);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1645,10 +1654,58 @@ static void file_change_d(struct branch *b)
|
|||||||
die("Garbage after path in: %s", command_buf.buf);
|
die("Garbage after path in: %s", command_buf.buf);
|
||||||
p = p_uq;
|
p = p_uq;
|
||||||
}
|
}
|
||||||
tree_content_remove(&b->branch_tree, p);
|
tree_content_remove(&b->branch_tree, p, NULL);
|
||||||
free(p_uq);
|
free(p_uq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void file_change_r(struct branch *b)
|
||||||
|
{
|
||||||
|
const char *s, *d;
|
||||||
|
char *s_uq, *d_uq;
|
||||||
|
const char *endp;
|
||||||
|
struct tree_entry leaf;
|
||||||
|
|
||||||
|
s = command_buf.buf + 2;
|
||||||
|
s_uq = unquote_c_style(s, &endp);
|
||||||
|
if (s_uq) {
|
||||||
|
if (*endp != ' ')
|
||||||
|
die("Missing space after source: %s", command_buf.buf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
endp = strchr(s, ' ');
|
||||||
|
if (!endp)
|
||||||
|
die("Missing space after source: %s", command_buf.buf);
|
||||||
|
s_uq = xmalloc(endp - s + 1);
|
||||||
|
memcpy(s_uq, s, endp - s);
|
||||||
|
s_uq[endp - s] = 0;
|
||||||
|
}
|
||||||
|
s = s_uq;
|
||||||
|
|
||||||
|
endp++;
|
||||||
|
if (!*endp)
|
||||||
|
die("Missing dest: %s", command_buf.buf);
|
||||||
|
|
||||||
|
d = endp;
|
||||||
|
d_uq = unquote_c_style(d, &endp);
|
||||||
|
if (d_uq) {
|
||||||
|
if (*endp)
|
||||||
|
die("Garbage after dest in: %s", command_buf.buf);
|
||||||
|
d = d_uq;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(&leaf, 0, sizeof(leaf));
|
||||||
|
tree_content_remove(&b->branch_tree, s, &leaf);
|
||||||
|
if (!leaf.versions[1].mode)
|
||||||
|
die("Path %s not in branch", s);
|
||||||
|
tree_content_set(&b->branch_tree, d,
|
||||||
|
leaf.versions[1].sha1,
|
||||||
|
leaf.versions[1].mode,
|
||||||
|
leaf.tree);
|
||||||
|
|
||||||
|
free(s_uq);
|
||||||
|
free(d_uq);
|
||||||
|
}
|
||||||
|
|
||||||
static void file_change_deleteall(struct branch *b)
|
static void file_change_deleteall(struct branch *b)
|
||||||
{
|
{
|
||||||
release_tree_content_recursive(b->branch_tree.tree);
|
release_tree_content_recursive(b->branch_tree.tree);
|
||||||
@ -1816,6 +1873,8 @@ static void cmd_new_commit(void)
|
|||||||
file_change_m(b);
|
file_change_m(b);
|
||||||
else if (!prefixcmp(command_buf.buf, "D "))
|
else if (!prefixcmp(command_buf.buf, "D "))
|
||||||
file_change_d(b);
|
file_change_d(b);
|
||||||
|
else if (!prefixcmp(command_buf.buf, "R "))
|
||||||
|
file_change_r(b);
|
||||||
else if (!strcmp("deleteall", command_buf.buf))
|
else if (!strcmp("deleteall", command_buf.buf))
|
||||||
file_change_deleteall(b);
|
file_change_deleteall(b);
|
||||||
else
|
else
|
||||||
|
@ -580,4 +580,72 @@ test_expect_success \
|
|||||||
git diff --raw L^ L >output &&
|
git diff --raw L^ L >output &&
|
||||||
git diff expect output'
|
git diff expect output'
|
||||||
|
|
||||||
|
###
|
||||||
|
### series M
|
||||||
|
###
|
||||||
|
|
||||||
|
test_tick
|
||||||
|
cat >input <<INPUT_END
|
||||||
|
commit refs/heads/M1
|
||||||
|
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
|
||||||
|
data <<COMMIT
|
||||||
|
file rename
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
from refs/heads/branch^0
|
||||||
|
R file2/newf file2/n.e.w.f
|
||||||
|
|
||||||
|
INPUT_END
|
||||||
|
|
||||||
|
cat >expect <<EOF
|
||||||
|
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 file2/newf file2/n.e.w.f
|
||||||
|
EOF
|
||||||
|
test_expect_success \
|
||||||
|
'M: rename file in same subdirectory' \
|
||||||
|
'git-fast-import <input &&
|
||||||
|
git diff-tree -M -r M1^ M1 >actual &&
|
||||||
|
compare_diff_raw expect actual'
|
||||||
|
|
||||||
|
cat >input <<INPUT_END
|
||||||
|
commit refs/heads/M2
|
||||||
|
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
|
||||||
|
data <<COMMIT
|
||||||
|
file rename
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
from refs/heads/branch^0
|
||||||
|
R file2/newf i/am/new/to/you
|
||||||
|
|
||||||
|
INPUT_END
|
||||||
|
|
||||||
|
cat >expect <<EOF
|
||||||
|
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 file2/newf i/am/new/to/you
|
||||||
|
EOF
|
||||||
|
test_expect_success \
|
||||||
|
'M: rename file to new subdirectory' \
|
||||||
|
'git-fast-import <input &&
|
||||||
|
git diff-tree -M -r M2^ M2 >actual &&
|
||||||
|
compare_diff_raw expect actual'
|
||||||
|
|
||||||
|
cat >input <<INPUT_END
|
||||||
|
commit refs/heads/M3
|
||||||
|
committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE
|
||||||
|
data <<COMMIT
|
||||||
|
file rename
|
||||||
|
COMMIT
|
||||||
|
|
||||||
|
from refs/heads/M2^0
|
||||||
|
R i other/sub
|
||||||
|
|
||||||
|
INPUT_END
|
||||||
|
|
||||||
|
cat >expect <<EOF
|
||||||
|
:100755 100755 f1fb5da718392694d0076d677d6d0e364c79b0bc f1fb5da718392694d0076d677d6d0e364c79b0bc R100 i/am/new/to/you other/sub/am/new/to/you
|
||||||
|
EOF
|
||||||
|
test_expect_success \
|
||||||
|
'M: rename subdirectory to new subdirectory' \
|
||||||
|
'git-fast-import <input &&
|
||||||
|
git diff-tree -M -r M3^ M3 >actual &&
|
||||||
|
compare_diff_raw expect actual'
|
||||||
|
|
||||||
test_done
|
test_done
|
||||||
|
Loading…
Reference in New Issue
Block a user