Skip to content

Commit

Permalink
Support wholesale directory renames in fast-import
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Shawn O. Pearce committed Jul 10, 2007
1 parent 11a2640 commit f39a946
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 19 deletions.
28 changes: 25 additions & 3 deletions Documentation/git-fast-import.txt
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ change to the project.
data
('from' SP <committish> LF)?
('merge' SP <committish> LF)?
(filemodify | filedelete | filedeleteall)*
(filemodify | filedelete | filerename | filedeleteall)*
LF
....

Expand All @@ -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
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
creating the commit. These commands may be supplied in any order.
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).

`author`
Expand Down Expand Up @@ -495,6 +497,26 @@ here `<path>` is the complete path of the file or subdirectory to
be removed from the branch.
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`
^^^^^^^^^^^^^^^
Included in a `commit` command to remove all files (and also all
Expand Down
91 changes: 75 additions & 16 deletions fast-import.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ Format of STDIN stream:
lf;
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_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_inm ::= 'M' sp mode sp 'inline' sp path_str lf
data;
Expand Down Expand Up @@ -1154,7 +1155,8 @@ static int tree_content_set(
struct tree_entry *root,
const char *p,
const unsigned char *sha1,
const uint16_t mode)
const uint16_t mode,
struct tree_content *subtree)
{
struct tree_content *t = root->tree;
const char *slash1;
Expand All @@ -1168,20 +1170,22 @@ static int tree_content_set(
n = strlen(p);
if (!n)
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++) {
e = t->entries[i];
if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) {
if (!slash1) {
if (e->versions[1].mode == mode
if (!S_ISDIR(mode)
&& e->versions[1].mode == mode
&& !hashcmp(e->versions[1].sha1, sha1))
return 0;
e->versions[1].mode = mode;
hashcpy(e->versions[1].sha1, sha1);
if (e->tree) {
if (e->tree)
release_tree_content_recursive(e->tree);
e->tree = NULL;
}
e->tree = subtree;
hashclr(root->versions[1].sha1);
return 1;
}
Expand All @@ -1191,7 +1195,7 @@ static int tree_content_set(
}
if (!e->tree)
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);
return 1;
}
Expand All @@ -1209,17 +1213,20 @@ static int tree_content_set(
if (slash1) {
e->tree = new_tree_content(8);
e->versions[1].mode = S_IFDIR;
tree_content_set(e, slash1 + 1, sha1, mode);
tree_content_set(e, slash1 + 1, sha1, mode, subtree);
} else {
e->tree = NULL;
e->tree = subtree;
e->versions[1].mode = mode;
hashcpy(e->versions[1].sha1, sha1);
}
hashclr(root->versions[1].sha1);
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;
const char *slash1;
Expand All @@ -1239,13 +1246,14 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
goto del_entry;
if (!e->tree)
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++) {
if (e->tree->entries[n]->versions[1].mode) {
hashclr(root->versions[1].sha1);
return 1;
}
}
backup_leaf = NULL;
goto del_entry;
}
return 0;
Expand All @@ -1254,10 +1262,11 @@ static int tree_content_remove(struct tree_entry *root, const char *p)
return 0;

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);
e->tree = NULL;
}
e->tree = NULL;
e->versions[1].mode = 0;
hashclr(e->versions[1].sha1);
hashclr(root->versions[1].sha1);
Expand Down Expand Up @@ -1629,7 +1638,7 @@ static void file_change_m(struct branch *b)
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);
}

Expand All @@ -1645,10 +1654,58 @@ static void file_change_d(struct branch *b)
die("Garbage after path in: %s", command_buf.buf);
p = p_uq;
}
tree_content_remove(&b->branch_tree, p);
tree_content_remove(&b->branch_tree, p, NULL);
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)
{
release_tree_content_recursive(b->branch_tree.tree);
Expand Down Expand Up @@ -1816,6 +1873,8 @@ static void cmd_new_commit(void)
file_change_m(b);
else if (!prefixcmp(command_buf.buf, "D "))
file_change_d(b);
else if (!prefixcmp(command_buf.buf, "R "))
file_change_r(b);
else if (!strcmp("deleteall", command_buf.buf))
file_change_deleteall(b);
else
Expand Down
68 changes: 68 additions & 0 deletions t/t9300-fast-import.sh
Original file line number Diff line number Diff line change
Expand Up @@ -580,4 +580,72 @@ test_expect_success \
git diff --raw L^ L >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

0 comments on commit f39a946

Please sign in to comment.