Page Menu
Home
Phabricator (Chris)
Search
Configure Global Search
Log In
Files
F620190
Git.cpp
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
Git.cpp
View Options
#include
"Git.h"
#include
<QDateTime>
#include
<QDebug>
#include
<QDir>
#include
<QProcess>
#include
<set>
#include
"joinpath.h"
#include
"misc.h"
#include
"LibGit2.h"
struct
Git
::
Private
{
QString
git_command
;
QByteArray
result
;
int
process_exit_code
=
0
;
QString
working_repo_dir
;
QString
error_message
;
};
Git
::
Git
(
const
Context
&
cx
,
QString
const
&
repodir
)
{
pv
=
new
Private
();
setGitCommand
(
cx
.
git_command
);
setWorkingRepositoryDir
(
repodir
);
}
Git
::~
Git
()
{
delete
pv
;
}
void
Git
::
setWorkingRepositoryDir
(
QString
const
&
repo
)
{
pv
->
working_repo_dir
=
repo
;
}
QString
const
&
Git
::
workingRepositoryDir
()
const
{
return
pv
->
working_repo_dir
;
}
bool
Git
::
isValidID
(
const
QString
&
s
)
{
int
n
=
s
.
size
();
if
(
n
>
0
)
{
ushort
const
*
p
=
s
.
utf16
();
for
(
int
i
=
0
;
i
<
n
;
i
++
)
{
uchar
c
=
QChar
::
toUpper
(
p
[
i
]);
if
((
c
>=
'0'
&&
c
<=
'9'
)
||
(
c
>=
'A'
&&
c
<=
'F'
))
{
// ok
}
else
{
return
false
;
}
}
return
true
;
}
return
false
;
}
QByteArray
Git
::
result
()
const
{
return
pv
->
result
;
}
void
Git
::
setGitCommand
(
QString
const
&
path
)
{
pv
->
git_command
=
path
;
}
QString
Git
::
gitCommand
()
const
{
return
pv
->
git_command
;
}
void
Git
::
clearResult
()
{
pv
->
result
.
clear
();
pv
->
process_exit_code
=
0
;
pv
->
error_message
=
QString
();
}
QString
Git
::
resultText
()
const
{
return
QString
::
fromUtf8
(
result
());
}
int
Git
::
getProcessExitCode
()
const
{
return
pv
->
process_exit_code
;
}
bool
Git
::
chdirexec
(
std
::
function
<
bool
()
>
fn
)
{
bool
ok
=
false
;
QDir
cwd
=
QDir
::
current
();
if
(
QDir
::
setCurrent
(
workingRepositoryDir
()))
{
ok
=
fn
();
QDir
::
setCurrent
(
cwd
.
path
());
}
return
ok
;
}
bool
Git
::
git
(
const
QString
&
arg
,
bool
chdir
)
{
QFileInfo
info
(
gitCommand
());
if
(
!
info
.
isExecutable
())
{
qDebug
()
<<
"Invalid git command: "
<<
gitCommand
();
return
false
;
}
#ifndef QT_NO_DEBUG
qDebug
()
<<
"exec: git "
<<
arg
;
#endif
clearResult
();
auto
Do
=
[
&
](){
QString
cmd
=
QString
(
"
\"
%1
\"
"
).
arg
(
gitCommand
());
cmd
+=
arg
;
QProcess
p
;
p
.
start
(
cmd
);
p
.
closeWriteChannel
();
p
.
setReadChannel
(
QProcess
::
StandardOutput
);
while
(
1
)
{
QProcess
::
ProcessState
s
=
p
.
state
();
if
(
p
.
waitForReadyRead
(
1
))
{
while
(
1
)
{
char
tmp
[
1024
];
qint64
len
=
p
.
read
(
tmp
,
sizeof
(
tmp
));
if
(
len
<
1
)
break
;
pv
->
result
.
append
(
tmp
,
len
);
}
}
else
if
(
s
==
QProcess
::
NotRunning
)
{
break
;
}
}
pv
->
process_exit_code
=
p
.
exitCode
();
#ifndef QT_NO_DEBUG
qDebug
()
<<
QString
(
"Process exit code: %1"
).
arg
(
getProcessExitCode
());
if
(
pv
->
process_exit_code
!=
0
)
{
pv
->
error_message
=
QString
::
fromUtf8
(
p
.
readAllStandardError
());
qDebug
()
<<
pv
->
error_message
;
}
#endif
return
pv
->
process_exit_code
==
0
;
};
if
(
chdir
)
{
return
chdirexec
(
Do
);
}
else
{
return
Do
();
}
}
QString
Git
::
errorMessage
()
const
{
return
pv
->
error_message
;
}
bool
Git
::
isValidWorkingCopy
(
QString
const
&
dir
)
{
return
QDir
(
dir
/
".git"
).
exists
();
}
bool
Git
::
isValidWorkingCopy
()
{
return
isValidWorkingCopy
(
workingRepositoryDir
());
}
QString
Git
::
version
()
{
git
(
"--version"
,
false
);
return
resultText
().
trimmed
();
}
QString
Git
::
revparseHEAD
()
{
git
(
"rev-parse HEAD "
);
return
resultText
().
trimmed
();
}
#if USE_LIBGIT2
QString
Git
::
diffHeadToWorkingDir_
()
{
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
std
::
string
s
=
r
.
diff_head_to_workdir
();
return
QString
::
fromStdString
(
s
);
}
QString
Git
::
diff_
(
QString
const
&
old_id
,
QString
const
&
new_id
)
{
if
(
old_id
.
isEmpty
()
&&
new_id
==
"HEAD"
)
{
return
diffHeadToWorkingDir_
();
}
else
{
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
std
::
string
s
=
r
.
diff2
(
old_id
.
toStdString
(),
new_id
.
toStdString
());
return
QString
::
fromStdString
(
s
);
}
}
#endif
QString
Git
::
diff
(
QString
const
&
old_id
,
QString
const
&
new_id
)
{
#if 1
QString
cmd
=
"diff --full-index -a %1 %2"
;
cmd
=
cmd
.
arg
(
old_id
).
arg
(
new_id
);
git
(
cmd
);
return
resultText
();
#else
return
diff_
(
old_id
,
new_id
);
#endif
}
QString
Git
::
diff_to_file
(
QString
const
&
old_id
,
QString
const
&
path
)
{
#if 1
QString
cmd
=
"diff --full-index -a %1 -- %2"
;
cmd
=
cmd
.
arg
(
old_id
).
arg
(
path
);
git
(
cmd
);
return
resultText
();
#else
return
diff_
(
old_id
,
new_id
);
#endif
}
QString
Git
::
getCurrentBranchName
()
{
if
(
isValidWorkingCopy
())
{
git
(
"rev-parse --abbrev-ref HEAD"
);
QString
s
=
resultText
().
trimmed
();
if
(
!
s
.
isEmpty
()
&&
!
s
.
startsWith
(
"fatal:"
)
&&
s
.
indexOf
(
' '
)
<
0
)
{
return
s
;
}
}
return
QString
();
}
QStringList
Git
::
getUntrackedFiles
()
{
QStringList
files
;
Git
::
FileStatusList
stats
=
status
();
for
(
FileStatus
const
&
s
:
stats
)
{
if
(
s
.
code
()
==
FileStatusCode
::
Untracked
)
{
files
.
push_back
(
s
.
path1
());
}
}
return
files
;
}
void
Git
::
parseAheadBehind
(
QString
const
&
s
,
Branch
*
b
)
{
ushort
const
*
begin
=
s
.
utf16
();
ushort
const
*
end
=
begin
+
s
.
size
();
ushort
const
*
ptr
=
begin
;
auto
NCompare
=
[](
ushort
const
*
a
,
char
const
*
b
,
size_t
n
){
for
(
size_t
i
=
0
;
i
<
n
;
i
++
)
{
if
(
*
a
<
(
unsigned
char
)
*
b
)
return
false
;
if
(
*
a
>
(
unsigned
char
)
*
b
)
return
false
;
if
(
!*
a
)
break
;
a
++
;
b
++
;
}
return
true
;
};
auto
SkipSpace
=
[
&
](){
while
(
ptr
<
end
&&
QChar
(
*
ptr
).
isSpace
())
{
ptr
++
;
}
};
auto
GetNumber
=
[
&
](){
int
n
=
0
;
while
(
ptr
<
end
&&
QChar
(
*
ptr
).
isDigit
())
{
n
=
n
*
10
+
(
*
ptr
-
'0'
);
ptr
++
;
}
return
n
;
};
if
(
*
ptr
==
'['
)
{
ptr
++
;
while
(
*
ptr
)
{
if
(
*
ptr
==
','
||
QChar
(
*
ptr
).
isSpace
())
{
ptr
++
;
}
else
if
(
NCompare
(
ptr
,
"ahead "
,
6
))
{
ptr
+=
6
;
SkipSpace
();
b
->
ahead
=
GetNumber
();
}
else
if
(
NCompare
(
ptr
,
"behind "
,
7
))
{
ptr
+=
7
;
SkipSpace
();
b
->
behind
=
GetNumber
();
}
else
{
break
;
}
}
}
}
QList
<
Git
::
Branch
>
Git
::
branches_
()
{
QList
<
Branch
>
branches
;
git
(
"branch -v -a --abbrev=40"
);
QString
s
=
resultText
();
#ifndef QT_NO_DEBUG
qDebug
()
<<
s
;
#endif
ushort
const
*
begin
=
s
.
utf16
();
ushort
const
*
end
=
begin
+
s
.
size
();
ushort
const
*
ptr
=
begin
;
while
(
ptr
+
2
<
end
)
{
ushort
attr
=
*
ptr
;
ptr
+=
2
;
QStringList
list
;
ushort
const
*
left
=
ptr
;
while
(
1
)
{
ushort
c
=
0
;
if
(
ptr
<
end
)
c
=
*
ptr
;
if
(
c
==
0
||
QChar
::
isSpace
(
c
))
{
if
(
left
<
ptr
)
{
list
.
push_back
(
QString
::
fromUtf16
(
left
,
ptr
-
left
));
if
(
list
.
size
()
==
2
)
{
while
(
ptr
<
end
&&
QChar
(
*
ptr
).
isSpace
())
ptr
++
;
left
=
ptr
;
while
(
ptr
<
end
)
{
ushort
c
=
*
ptr
;
ptr
++
;
if
(
c
==
'\n'
||
c
==
'\r'
)
{
list
.
push_back
(
QString
::
fromUtf16
(
left
,
ptr
-
left
));
break
;
}
}
c
=
0
;
}
}
if
(
c
==
0
)
break
;
ptr
++
;
if
(
c
==
'\n'
||
c
==
'\r'
)
break
;
left
=
ptr
;
}
else
{
ptr
++
;
}
}
if
(
list
.
size
()
>=
2
)
{
Branch
b
;
b
.
name
=
list
[
0
];
if
(
list
[
1
].
size
()
==
40
)
{
b
.
id
=
list
[
1
];
if
(
list
.
size
()
>
2
)
{
parseAheadBehind
(
list
[
2
],
&
b
);
}
}
else
if
(
list
[
1
]
==
"->"
)
{
if
(
list
.
size
()
>=
3
)
{
b
.
id
=
">"
+
list
[
2
];
}
}
if
(
attr
==
'*'
)
{
b
.
flags
|=
Branch
::
Current
;
}
branches
.
push_back
(
b
);
}
}
for
(
int
i
=
0
;
i
<
branches
.
size
();
i
++
)
{
Branch
*
b
=
&
branches
[
i
];
if
(
b
->
id
.
startsWith
(
'>'
))
{
QString
name
=
b
->
id
.
mid
(
1
);
if
(
name
.
startsWith
(
"origin/"
))
{
name
=
name
.
mid
(
7
);
}
for
(
int
j
=
0
;
j
<
branches
.
size
();
j
++
)
{
if
(
j
!=
i
)
{
if
(
branches
[
j
].
name
==
name
)
{
branches
[
i
].
id
=
branches
[
j
].
id
;
break
;
}
}
}
}
}
return
branches
;
}
QList
<
Git
::
Branch
>
Git
::
branches
()
{
#if 1
return
branches_
();
#else
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
return
r
.
branches
();
#endif
}
Git
::
CommitItemList
Git
::
log_all
(
QString
const
&
id
,
int
maxcount
)
{
QDateTime
limit_time
=
QDateTime
::
currentDateTime
();
limit_time
=
limit_time
.
addYears
(
-3
);
CommitItemList
items
;
QString
text
;
#if 1
QString
cmd
=
"log --pretty=format:
\"
commit:%H#parent:%P#author:%an#mail:%ae#date:%ci##%s
\"
--all -%1 %2"
;
cmd
=
cmd
.
arg
(
maxcount
).
arg
(
id
);
git
(
cmd
);
text
=
resultText
().
trimmed
();
if
(
0
)
{
QFile
file
(
"test.log"
);
if
(
file
.
open
(
QFile
::
WriteOnly
))
{
file
.
write
(
text
.
toUtf8
());
}
}
#else
{
QFile
file
(
"test.log"
);
if
(
file
.
open
(
QFile
::
ReadOnly
))
{
text
=
QString
::
fromUtf8
(
file
.
readAll
());
}
}
#endif
QStringList
lines
=
misc
::
splitLines
(
text
);
for
(
QString
const
&
line
:
lines
)
{
int
i
=
line
.
indexOf
(
"##"
);
if
(
i
>
0
)
{
Git
::
CommitItem
item
;
item
.
message
=
line
.
mid
(
i
+
2
);
QStringList
atts
=
line
.
mid
(
0
,
i
).
split
(
'#'
);
for
(
QString
const
&
s
:
atts
)
{
int
j
=
s
.
indexOf
(
':'
);
if
(
j
>
0
)
{
QString
key
=
s
.
mid
(
0
,
j
);
QString
val
=
s
.
mid
(
j
+
1
);
if
(
key
==
"commit"
)
{
item
.
commit_id
=
val
;
}
else
if
(
key
==
"parent"
)
{
item
.
parent_ids
=
val
.
split
(
' '
,
QString
::
SkipEmptyParts
);
}
else
if
(
key
==
"author"
)
{
item
.
author
=
val
;
}
else
if
(
key
==
"mail"
)
{
item
.
mail
=
val
;
}
else
if
(
key
==
"date"
)
{
item
.
commit_date
=
QDateTime
::
fromString
(
val
,
Qt
::
ISODate
).
toLocalTime
();
}
else
if
(
key
==
"debug"
)
{
}
}
}
if
(
item
.
commit_date
<
limit_time
)
{
break
;
}
items
.
push_back
(
item
);
}
}
return
std
::
move
(
items
);
}
Git
::
CommitItemList
Git
::
log
(
int
maxcount
)
{
#if 1
return
log_all
(
QString
(),
maxcount
);
#else
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
return
r
.
log
(
maxcount
);
#endif
}
bool
Git
::
clone
(
QString
const
&
location
,
QString
const
&
path
)
{
QString
basedir
;
QString
subdir
;
if
(
path
.
endsWith
(
'/'
)
||
path
.
endsWith
(
'\\'
))
{
auto
GitBaseName
=
[](
QString
location
){
int
i
=
location
.
lastIndexOf
(
'/'
);
int
j
=
location
.
lastIndexOf
(
'\\'
);
if
(
i
<
j
)
i
=
j
;
i
=
i
<
0
?
0
:
(
i
+
1
);
j
=
location
.
size
();
if
(
location
.
endsWith
(
".git"
))
{
j
-=
4
;
}
if
(
i
<
j
)
{
location
=
location
.
mid
(
i
,
j
-
i
);
}
return
location
;
};
basedir
=
path
;
subdir
=
GitBaseName
(
location
);
}
else
{
QFileInfo
info
(
path
);
basedir
=
info
.
dir
().
path
();
subdir
=
info
.
fileName
();
}
pv
->
working_repo_dir
=
misc
::
normalizePathSeparator
(
basedir
/
subdir
);
bool
ok
=
false
;
QDir
cwd
=
QDir
::
current
();
if
(
QDir
::
setCurrent
(
basedir
))
{
QString
cmd
=
"clone
\"
%1
\"
\"
%2
\"
"
;
cmd
=
cmd
.
arg
(
location
).
arg
(
subdir
);
ok
=
git
(
cmd
,
false
);
QDir
::
setCurrent
(
cwd
.
path
());
}
return
ok
;
}
bool
Git
::
commit_
(
QString
const
&
msg
,
bool
amend
)
{
QString
cmd
=
"commit"
;
if
(
amend
)
{
cmd
+=
" --amend"
;
}
int
n
=
0
;
QString
text
=
msg
.
trimmed
();
if
(
!
text
.
isEmpty
())
{
QStringList
lines
=
misc
::
splitLines
(
text
);
for
(
QString
const
&
line
:
lines
)
{
QString
s
=
line
.
trimmed
();
s
=
s
.
replace
(
'|'
,
' '
);
s
=
s
.
replace
(
'<'
,
' '
);
s
=
s
.
replace
(
'>'
,
' '
);
s
=
s
.
replace
(
'\"'
,
'\''
);
if
(
n
>
0
||
!
s
.
isEmpty
())
{
n
++
;
}
cmd
+=
QString
(
" -m
\"
%1
\"
"
).
arg
(
s
);
}
}
if
(
n
==
0
)
{
cmd
+=
" -m
\"
no message
\"
"
;
}
return
git
(
cmd
);
}
bool
Git
::
commit
(
QString
const
&
text
)
{
#if 1
return
commit_
(
text
,
false
);
#else
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
workingRepositoryDir
().
toStdString
());
return
r
.
commit
(
text
.
toStdString
());
#endif
}
bool
Git
::
commit_amend_m
(
const
QString
&
text
)
{
return
commit_
(
text
,
true
);
}
void
Git
::
push_
()
{
git
(
"push"
);
}
void
Git
::
push
()
{
#if 1
push_
();
#else
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
workingRepositoryDir
().
toStdString
());
r
.
push_
();
#endif
}
Git
::
FileStatusList
Git
::
status_
()
{
FileStatusList
files
;
if
(
git
(
"status -s -u --porcelain"
))
{
QString
text
=
resultText
();
QStringList
lines
=
misc
::
splitLines
(
text
);
for
(
QString
const
&
line
:
lines
)
{
if
(
line
.
size
()
>
3
)
{
FileStatus
s
(
line
);
if
(
s
.
code
()
!=
FileStatusCode
::
Unknown
)
{
files
.
push_back
(
s
);
}
}
}
}
return
std
::
move
(
files
);
}
Git
::
FileStatusList
Git
::
status
()
{
#if 1
return
status_
();
#else
std
::
string
repodir
=
workingRepositoryDir
().
toStdString
();
return
LibGit2
::
status
(
repodir
);
#endif
}
QByteArray
Git
::
cat_file_
(
QString
const
&
id
)
{
#if 1
git
(
"cat-file -p "
+
id
);
return
pv
->
result
;
#else
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
return
r
.
cat_file
(
id
.
toStdString
());
#endif
}
bool
Git
::
cat_file
(
QString
const
&
id
,
QByteArray
*
out
)
{
if
(
isValidID
(
id
))
{
*
out
=
cat_file_
(
id
);
return
true
;
}
return
false
;
}
void
Git
::
revertFile
(
const
QString
&
path
)
{
#if 1
git
(
"checkout -- "
+
path
);
#else
std
::
string
dir
=
workingRepositoryDir
().
toStdString
();
LibGit2
::
Repository
r
=
LibGit2
::
openRepository
(
dir
);
r
.
revert_a_file
(
path
.
toStdString
());
#endif
}
void
Git
::
revertAllFiles
()
{
git
(
"reset --hard HEAD"
);
}
void
Git
::
removeFile
(
const
QString
&
path
)
{
git
(
"rm "
+
path
);
}
void
Git
::
stage
(
QString
const
&
path
)
{
git
(
"add "
+
path
);
}
void
Git
::
stage
(
QStringList
const
&
paths
)
{
QString
cmd
=
"add"
;
for
(
QString
const
&
path
:
paths
)
{
cmd
+=
"
\"
"
;
cmd
+=
path
;
cmd
+=
"
\"
"
;
}
git
(
cmd
);
}
void
Git
::
unstage
(
QString
const
&
path
)
{
git
(
"reset HEAD "
+
path
);
}
void
Git
::
unstage
(
QStringList
const
&
paths
)
{
QString
cmd
=
"reset HEAD"
;
for
(
QString
const
&
path
:
paths
)
{
cmd
+=
' '
;
cmd
+=
path
;
}
git
(
cmd
);
}
void
Git
::
pull
()
{
git
(
"pull"
);
}
void
Git
::
fetch
()
{
git
(
"fetch"
);
}
QStringList
Git
::
make_branch_list_
()
{
QStringList
list
;
QStringList
l
=
misc
::
splitLines
(
resultText
());
for
(
QString
const
&
s
:
l
)
{
if
(
s
.
startsWith
(
"* "
))
list
.
push_back
(
s
.
mid
(
2
));
if
(
s
.
startsWith
(
" "
))
list
.
push_back
(
s
.
mid
(
2
));
}
return
list
;
}
void
Git
::
createBranch
(
QString
const
&
name
)
{
git
(
"branch "
+
name
);
}
void
Git
::
checkoutBranch
(
QString
const
&
name
)
{
git
(
"checkout "
+
name
);
}
void
Git
::
mergeBranch
(
QString
const
&
name
)
{
git
(
"merge "
+
name
);
}
void
Git
::
test
()
{
}
// Git::FileStatus
QString
Git
::
trimPath
(
QString
const
&
s
)
{
ushort
const
*
begin
=
s
.
utf16
();
ushort
const
*
end
=
begin
+
s
.
size
();
ushort
const
*
left
=
begin
;
ushort
const
*
right
=
end
;
while
(
left
<
right
&&
QChar
(
*
left
).
isSpace
())
left
++
;
while
(
left
<
right
&&
QChar
(
right
[
-1
]).
isSpace
())
right
--
;
if
(
left
+
1
<
right
&&
*
left
==
'\"'
&&
right
[
-1
]
==
'\"'
)
{
// if quoted ?
left
++
;
right
--
;
QByteArray
ba
;
ushort
const
*
ptr
=
left
;
while
(
ptr
<
right
)
{
ushort
c
=
*
ptr
;
ptr
++
;
if
(
c
==
'\\'
)
{
c
=
0
;
while
(
ptr
<
right
&&
QChar
(
*
ptr
).
isDigit
())
{
// decode \oct
c
=
c
*
8
+
(
*
ptr
-
'0'
);
ptr
++
;
}
}
ba
.
push_back
(
c
);
}
return
QString
::
fromUtf8
(
ba
);
}
if
(
left
==
begin
&&
right
==
end
)
return
s
;
return
QString
::
fromUtf16
(
left
,
right
-
left
);
}
void
Git
::
FileStatus
::
parse
(
const
QString
&
text
)
{
if
(
text
.
size
()
>
3
)
{
ushort
const
*
p
=
text
.
utf16
();
if
(
p
[
2
]
==
' '
)
{
data
.
code_x
=
p
[
0
];
data
.
code_y
=
p
[
1
];
int
i
=
text
.
indexOf
(
" -> "
,
3
);
if
(
i
>
3
)
{
data
.
path1
=
trimPath
(
text
.
mid
(
3
,
i
-
3
));
data
.
path2
=
trimPath
(
text
.
mid
(
i
+
4
));
}
else
{
data
.
path1
=
trimPath
(
text
.
mid
(
3
));
data
.
path2
=
QString
();
}
data
.
code
=
parseFileStatusCode
(
data
.
code_x
,
data
.
code_y
);
}
}
}
//
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Sat, Nov 14, 5:41 PM (14 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
151941
Default Alt Text
Git.cpp (14 KB)
Attached To
Mode
R77 Guitar
Attached
Detach File
Event Timeline