Skip to content

Commit b3fbf52

Browse files
taidaiimeta-codesync[bot]
authored andcommitted
index: move code operating git index into gitcompat::index
Reviewed By: vilatto Differential Revision: D93046680 fbshipit-source-id: 9f1eb8643bfcf038e119baffe20b2684c79d8201
1 parent aec37de commit b3fbf52

File tree

3 files changed

+142
-123
lines changed

3 files changed

+142
-123
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
use std::io;
9+
use std::process::ExitStatus;
10+
11+
use spawn_ext::CommandExt;
12+
use types::HgId;
13+
14+
use crate::RepoGit;
15+
use crate::rungit::GitCmd;
16+
17+
/// Convert `git diff-index --norenames --raw -z <tree-ish>` output
18+
/// into `git update-index -z --index-info` input.
19+
///
20+
/// diff-index entry format:
21+
/// `:<old_mode> <new_mode> <old_sha> <new_sha> <status>\0<path>\0`
22+
///
23+
/// See https://git-scm.com/docs/git-diff-index#_raw_output_format
24+
///
25+
/// `git update-index --index-info` format:
26+
/// `<old_mode> <old_sha> <stage>\t<path>\0`
27+
///
28+
/// See https://git-scm.com/docs/git-update-index#_using_index_info
29+
fn diff_index_to_index_info(raw: &[u8]) -> io::Result<Vec<u8>> {
30+
let mut result = Vec::new();
31+
let mut pos = 0;
32+
33+
let parse_err = |msg: &str| io::Error::new(io::ErrorKind::InvalidData, msg.to_owned());
34+
35+
while pos < raw.len() {
36+
// Each entry starts with ':'
37+
if raw[pos] != b':' {
38+
return Err(parse_err("missing ':' at the beginning"));
39+
}
40+
pos += 1;
41+
42+
// Header ends at the first \0: "<old_mode> <new_mode> <old_sha> <new_sha> <status>"
43+
let header_end = raw[pos..]
44+
.iter()
45+
.position(|&b| b == 0)
46+
.ok_or_else(|| parse_err("no NUL in the entry"))?
47+
+ pos;
48+
49+
let header =
50+
std::str::from_utf8(&raw[pos..header_end]).map_err(|e| parse_err(&e.to_string()))?;
51+
52+
// Split: old_mode, new_mode, old_sha, new_sha, status
53+
let mut parts = header.splitn(5, ' ');
54+
let old_mode = parts.next().ok_or_else(|| parse_err("missing old_mode"))?;
55+
parts.next(); // new_mode
56+
let old_sha = parts.next().ok_or_else(|| parse_err("missing old_sha"))?;
57+
parts.next(); // new_sha
58+
let status = parts.next().ok_or_else(|| parse_err("missing status"))?;
59+
60+
// Copy (C) and rename (R) should be opted out by the diff-index command.
61+
// Copied and renamed files show up as addition (A) and deletion (D) instead.
62+
// Unmerged (U) status should not show up as Sapling does not expose merge conflicts to Git.
63+
match status {
64+
"M" | "A" | "D" | "T" => {}
65+
_ => {
66+
return Err(parse_err(&format!(
67+
"unexpected diff-index status: {status}"
68+
)));
69+
}
70+
}
71+
72+
// Remaining: \0<path>\0
73+
pos = header_end + 1;
74+
let path_end = raw[pos..]
75+
.iter()
76+
.position(|&b| b == 0)
77+
.ok_or_else(|| parse_err("missing NUL after path"))?
78+
+ pos;
79+
let path = &raw[pos..path_end];
80+
pos = path_end + 1;
81+
82+
// <mode>SP<sha1>SP<stage>TAB<path>
83+
result.extend_from_slice(old_mode.as_bytes());
84+
result.push(b' ');
85+
result.extend_from_slice(old_sha.as_bytes());
86+
result.extend_from_slice(b" 0\t");
87+
result.extend_from_slice(path);
88+
result.push(b'\0');
89+
}
90+
91+
Ok(result)
92+
}
93+
94+
impl RepoGit {
95+
/// Update git index for mutated paths compared to given commit.
96+
/// Uses `--index-info` to avoid command-line argument length limits.
97+
pub fn update_diff_index(&self, treeish: HgId) -> io::Result<ExitStatus> {
98+
let hex = treeish.to_hex();
99+
let output = self.call(
100+
"diff-index",
101+
&["--cached", "--no-renames", "--raw", "-z", &hex],
102+
)?;
103+
104+
let index_info = diff_index_to_index_info(&output.stdout)?;
105+
106+
let mut cmd = self.git_cmd("update-index", &["-z", "--index-info"]);
107+
cmd.checked_run_with_stdin(&index_info)
108+
}
109+
}
110+
111+
#[cfg(test)]
112+
mod tests {
113+
use super::*;
114+
115+
#[test]
116+
fn test_diff_index_to_index_info() {
117+
// Empty input
118+
assert_eq!(diff_index_to_index_info(b"").unwrap(), b"");
119+
120+
let raw = b":100644 100644 aaaa bbbb M\0modifiedfile\0\
121+
:000000 100644 0000 bbbb A\0addedfile\0\
122+
:100755 000000 aaaa 0000 D\0deletedfile\0";
123+
let out = diff_index_to_index_info(raw).unwrap();
124+
assert_eq!(
125+
out,
126+
b"100644 aaaa 0\tmodifiedfile\0\
127+
000000 0000 0\taddedfile\0\
128+
100755 aaaa 0\tdeletedfile\0"
129+
);
130+
}
131+
132+
#[test]
133+
fn test_diff_index_with_unexpected_status() {
134+
let raw = b":000000 000000 0000 0000 U\0unmergedfile\0";
135+
let err = diff_index_to_index_info(raw).unwrap_err();
136+
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
137+
assert!(err.to_string().contains("unexpected diff-index status: U"));
138+
}
139+
}

eden/scm/lib/gitcompat/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pub mod rungit;
5757
/// Work with git references.
5858
mod refs;
5959

60+
/// Work with the git index.
61+
mod index;
62+
6063
/// Work with git configs.
6164
mod config;
6265

eden/scm/lib/gitcompat/src/rungit.rs

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use configmodel::Config;
1919
use configmodel::ConfigExt;
2020
use identity::dotgit::follow_dotgit_path;
2121
use spawn_ext::CommandExt;
22-
use types::HgId;
2322

2423
/// Run `git` outside a repo.
2524
#[derive(Default, Clone)]
@@ -185,98 +184,6 @@ impl RepoGit {
185184
pub fn root(&self) -> &Path {
186185
&self.root
187186
}
188-
189-
/// Convert `git diff-index --norenames --raw -z <tree-ish>` output
190-
/// into `git update-index -z --index-info` input.
191-
///
192-
/// diff-index entry format:
193-
/// `:<old_mode> <new_mode> <old_sha> <new_sha> <status>\0<path>\0`
194-
///
195-
/// See https://git-scm.com/docs/git-diff-index#_raw_output_format
196-
///
197-
/// `git update-index --index-info` format:
198-
/// `<old_mode> <old_sha> <stage>\t<path>\0`
199-
///
200-
/// See https://git-scm.com/docs/git-update-index#_using_index_info
201-
fn diff_index_to_index_info(raw: &[u8]) -> io::Result<Vec<u8>> {
202-
let mut result = Vec::new();
203-
let mut pos = 0;
204-
205-
let parse_err = |msg: &str| io::Error::new(io::ErrorKind::InvalidData, msg.to_owned());
206-
207-
while pos < raw.len() {
208-
// Each entry starts with ':'
209-
if raw[pos] != b':' {
210-
return Err(parse_err("missing ':' at the beginning"));
211-
}
212-
pos += 1;
213-
214-
// Header ends at the first \0: "<old_mode> <new_mode> <old_sha> <new_sha> <status>"
215-
let header_end = raw[pos..]
216-
.iter()
217-
.position(|&b| b == 0)
218-
.ok_or_else(|| parse_err("no NUL in the entry"))?
219-
+ pos;
220-
221-
let header = std::str::from_utf8(&raw[pos..header_end])
222-
.map_err(|e| parse_err(&e.to_string()))?;
223-
224-
// Split: old_mode, new_mode, old_sha, new_sha, status
225-
let mut parts = header.splitn(5, ' ');
226-
let old_mode = parts.next().ok_or_else(|| parse_err("missing old_mode"))?;
227-
parts.next(); // new_mode
228-
let old_sha = parts.next().ok_or_else(|| parse_err("missing old_sha"))?;
229-
parts.next(); // new_sha
230-
let status = parts.next().ok_or_else(|| parse_err("missing status"))?;
231-
232-
// Copy (C) and rename (R) should be opted out by the diff-index command.
233-
// Copied and renamed files show up as addition (A) and deletion (D) instead.
234-
// Unmerged (U) status should not show up as Sapling does not expose merge conflicts to Git.
235-
match status {
236-
"M" | "A" | "D" | "T" => {}
237-
_ => {
238-
return Err(parse_err(&format!(
239-
"unexpected diff-index status: {status}"
240-
)));
241-
}
242-
}
243-
244-
// Remaining: \0<path>\0
245-
pos = header_end + 1;
246-
let path_end = raw[pos..]
247-
.iter()
248-
.position(|&b| b == 0)
249-
.ok_or_else(|| parse_err("missing NUL after path"))?
250-
+ pos;
251-
let path = &raw[pos..path_end];
252-
pos = path_end + 1;
253-
254-
// <mode>SP<sha1>SP<stage>TAB<path>
255-
result.extend_from_slice(old_mode.as_bytes());
256-
result.push(b' ');
257-
result.extend_from_slice(old_sha.as_bytes());
258-
result.extend_from_slice(b" 0\t");
259-
result.extend_from_slice(path);
260-
result.push(b'\0');
261-
}
262-
263-
Ok(result)
264-
}
265-
266-
/// Update git index for mutated paths compared to given commit.
267-
/// Uses `--index-info` to avoid command-line argument length limits.
268-
pub fn update_diff_index(&self, treeish: HgId) -> io::Result<ExitStatus> {
269-
let hex = treeish.to_hex();
270-
let output = self.call(
271-
"diff-index",
272-
&["--cached", "--no-renames", "--raw", "-z", &hex],
273-
)?;
274-
275-
let index_info = Self::diff_index_to_index_info(&output.stdout)?;
276-
277-
let mut cmd = self.git_cmd("update-index", &["-z", "--index-info"]);
278-
cmd.checked_run_with_stdin(&index_info)
279-
}
280187
}
281188

282189
impl Deref for BareGit {
@@ -417,33 +324,3 @@ fn git_cmd_impl(
417324

418325
cmd
419326
}
420-
421-
#[cfg(test)]
422-
mod tests {
423-
use super::*;
424-
425-
#[test]
426-
fn test_diff_index_to_index_info() {
427-
// Empty input
428-
assert_eq!(RepoGit::diff_index_to_index_info(b"").unwrap(), b"");
429-
430-
let raw = b":100644 100644 aaaa bbbb M\0modifiedfile\0\
431-
:000000 100644 0000 bbbb A\0addedfile\0\
432-
:100755 000000 aaaa 0000 D\0deletedfile\0";
433-
let out = RepoGit::diff_index_to_index_info(raw).unwrap();
434-
assert_eq!(
435-
out,
436-
b"100644 aaaa 0\tmodifiedfile\0\
437-
000000 0000 0\taddedfile\0\
438-
100755 aaaa 0\tdeletedfile\0"
439-
);
440-
}
441-
442-
#[test]
443-
fn test_diff_index_with_unexpected_status() {
444-
let raw = b":000000 000000 0000 0000 U\0unmergedfile\0";
445-
let err = RepoGit::diff_index_to_index_info(raw).unwrap_err();
446-
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
447-
assert!(err.to_string().contains("unexpected diff-index status: U"));
448-
}
449-
}

0 commit comments

Comments
 (0)