Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2892750
ArcanistSubversionAPI.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
16 KB
Referenced Files
None
Subscribers
None
ArcanistSubversionAPI.php
View Options
<?php
/**
* Interfaces with Subversion working copies.
*
* @group workingcopy
*/
final
class
ArcanistSubversionAPI
extends
ArcanistRepositoryAPI
{
protected
$svnStatus
;
protected
$svnBaseRevisions
;
protected
$svnInfo
=
array
(
)
;
protected
$svnInfoRaw
=
array
(
)
;
protected
$svnDiffRaw
=
array
(
)
;
private
$svnBaseRevisionNumber
;
public
function
getSourceControlSystemName
(
)
{
return
'svn'
;
}
public
function
getMetadataPath
(
)
{
static
$svn_dir
=
null
;
if
(
$svn_dir
===
null
)
{
// from svn 1.7, subversion keeps a single .svn directly under
// the working copy root. However, we allow .arcconfigs that
// aren't at the working copy root.
foreach
(
Filesystem
::
walkToRoot
(
$this
->
getPath
(
)
)
as
$parent
)
{
$possible_svn_dir
=
Filesystem
::
resolvePath
(
'.svn'
,
$parent
)
;
if
(
Filesystem
::
pathExists
(
$possible_svn_dir
)
)
{
$svn_dir
=
$possible_svn_dir
;
break
;
}
}
}
return
$svn_dir
;
}
protected
function
buildLocalFuture
(
array
$argv
)
{
$argv
[
0
]
=
'svn '
.
$argv
[
0
]
;
$future
=
newv
(
'ExecFuture'
,
$argv
)
;
$future
->
setCWD
(
$this
->
getPath
(
)
)
;
return
$future
;
}
public
function
getWorkingCopyStatus
(
)
{
return
$this
->
getSVNStatus
(
)
;
}
public
function
getSVNBaseRevisions
(
)
{
if
(
$this
->
svnBaseRevisions
===
null
)
{
$this
->
getSVNStatus
(
)
;
}
return
$this
->
svnBaseRevisions
;
}
public
function
getSVNStatus
(
$with_externals
=
false
)
{
if
(
$this
->
svnStatus
===
null
)
{
list
(
$status
)
=
$this
->
execxLocal
(
'--xml status'
)
;
$xml
=
new
SimpleXMLElement
(
$status
)
;
if
(
count
(
$xml
->
target
)
!=
1
)
{
throw
new
Exception
(
"Expected exactly one XML status target."
)
;
}
$externals
=
array
(
)
;
$files
=
array
(
)
;
$target
=
$xml
->
target
[
0
]
;
$this
->
svnBaseRevisions
=
array
(
)
;
foreach
(
$target
->
entry
as
$entry
)
{
$path
=
(string)
$entry
[
'path'
]
;
$mask
=
0
;
$props
=
(string)
(
$entry
->
{
'wc-status'
}
[
0
]
[
'props'
]
)
;
$item
=
(string)
(
$entry
->
{
'wc-status'
}
[
0
]
[
'item'
]
)
;
$base
=
(string)
(
$entry
->
{
'wc-status'
}
[
0
]
[
'revision'
]
)
;
$this
->
svnBaseRevisions
[
$path
]
=
$base
;
switch
(
$props
)
{
case
'none'
:
case
'normal'
:
break
;
case
'modified'
:
$mask
|=
self
::
FLAG_MODIFIED
;
break
;
default
:
throw
new
Exception
(
"Unrecognized property status '{$props}'."
)
;
}
$mask
|=
$this
->
parseSVNStatus
(
$item
)
;
if
(
$item
==
'external'
)
{
$externals
[
]
=
$path
;
}
// This is new in or around Subversion 1.6.
$tree_conflicts
=
(string)
(
$entry
->
{
'wc-status'
}
[
0
]
[
'tree-conflicted'
]
)
;
if
(
$tree_conflicts
)
{
$mask
|=
self
::
FLAG_CONFLICT
;
}
$files
[
$path
]
=
$mask
;
}
foreach
(
$files
as
$path
=>
$mask
)
{
foreach
(
$externals
as
$external
)
{
if
(
!
strncmp
(
$path
,
$external
,
strlen
(
$external
)
)
)
{
$files
[
$path
]
|=
self
::
FLAG_EXTERNALS
;
}
}
}
$this
->
svnStatus
=
$files
;
}
$status
=
$this
->
svnStatus
;
if
(
!
$with_externals
)
{
foreach
(
$status
as
$path
=>
$mask
)
{
if
(
$mask
&
ArcanistRepositoryAPI
::
FLAG_EXTERNALS
)
{
unset
(
$status
[
$path
]
)
;
}
}
}
return
$status
;
}
private
function
parseSVNStatus
(
$item
)
{
switch
(
$item
)
{
case
'normal'
:
return
0
;
case
'external'
:
return
self
::
FLAG_EXTERNALS
;
case
'unversioned'
:
return
self
::
FLAG_UNTRACKED
;
case
'obstructed'
:
return
self
::
FLAG_OBSTRUCTED
;
case
'missing'
:
return
self
::
FLAG_MISSING
;
case
'added'
:
return
self
::
FLAG_ADDED
;
case
'replaced'
:
// This is the result of "svn rm"-ing a file, putting another one
// in place of it, and then "svn add"-ing the new file. Just treat
// this as equivalent to "modified".
return
self
::
FLAG_MODIFIED
;
case
'modified'
:
return
self
::
FLAG_MODIFIED
;
case
'deleted'
:
return
self
::
FLAG_DELETED
;
case
'conflicted'
:
return
self
::
FLAG_CONFLICT
;
case
'incomplete'
:
return
self
::
FLAG_INCOMPLETE
;
default
:
throw
new
Exception
(
"Unrecognized item status '{$item}'."
)
;
}
}
public
function
addToCommit
(
array
$paths
)
{
$this
->
execxLocal
(
'add -- %Ls'
,
$paths
)
;
}
public
function
getSVNProperty
(
$path
,
$property
)
{
list
(
$stdout
)
=
execx
(
'svn propget %s %s@'
,
$property
,
$this
->
getPath
(
$path
)
)
;
return
trim
(
$stdout
)
;
}
public
function
getSourceControlPath
(
)
{
return
idx
(
$this
->
getSVNInfo
(
'/'
)
,
'URL'
)
;
}
public
function
getSourceControlBaseRevision
(
)
{
$info
=
$this
->
getSVNInfo
(
'/'
)
;
return
$info
[
'URL'
]
.
'@'
.
$this
->
getSVNBaseRevisionNumber
(
)
;
}
public
function
getCanonicalRevisionName
(
$string
)
{
throw
new
ArcanistCapabilityNotSupportedException
(
$this
)
;
}
public
function
getSVNBaseRevisionNumber
(
)
{
if
(
$this
->
svnBaseRevisionNumber
)
{
return
$this
->
svnBaseRevisionNumber
;
}
$info
=
$this
->
getSVNInfo
(
'/'
)
;
return
$info
[
'Revision'
]
;
}
public
function
overrideSVNBaseRevisionNumber
(
$effective_base_revision
)
{
$this
->
svnBaseRevisionNumber
=
$effective_base_revision
;
return
$this
;
}
public
function
getBranchName
(
)
{
return
'svn'
;
}
public
function
buildInfoFuture
(
$path
)
{
if
(
$path
==
'/'
)
{
// When the root of a working copy is referenced by a symlink and you
// execute 'svn info' on that symlink, svn fails. This is a longstanding
// bug in svn:
//
// See http://subversion.tigris.org/issues/show_bug.cgi?id=2305
//
// To reproduce, do:
//
// $ ln -s working_copy working_link
// $ svn info working_copy # ok
// $ svn info working_link # fails
//
// Work around this by cd-ing into the directory before executing
// 'svn info'.
return
$this
->
buildLocalFuture
(
array
(
'info .'
)
)
;
}
else
{
// Note: here and elsewhere we need to append "@" to the path because if
// a file has a literal "@" in it, everything after that will be
// interpreted as a revision. By appending "@" with no argument, SVN
// parses it properly.
return
$this
->
buildLocalFuture
(
array
(
'info %s@'
,
$this
->
getPath
(
$path
)
)
)
;
}
}
public
function
buildDiffFuture
(
$path
)
{
// The "--depth empty" flag prevents us from picking up changes in
// children when we run 'diff' against a directory. Specifically, when a
// user has added or modified some directory "example/", we want to return
// ONLY changes to that directory when given it as a path. If we run
// without "--depth empty", svn will give us changes to the directory
// itself (such as property changes) and also give us changes to any
// files within the directory (basically, implicit recursion). We don't
// want that, so prevent recursive diffing.
$root
=
phutil_get_library_root
(
'arcanist'
)
;
if
(
phutil_is_windows
(
)
)
{
// TODO: Provide a binary_safe_diff script for Windows.
// TODO: Provide a diff command which can take lines of context somehow.
return
$this
->
buildLocalFuture
(
array
(
'diff --depth empty %s'
,
$path
,
)
)
;
}
else
{
$diff_bin
=
$root
.
'/../scripts/repository/binary_safe_diff.sh'
;
$diff_cmd
=
Filesystem
::
resolvePath
(
$diff_bin
)
;
return
$this
->
buildLocalFuture
(
array
(
'diff --depth empty --diff-cmd %s -x -U%d %s'
,
$diff_cmd
,
$this
->
getDiffLinesOfContext
(
)
,
$path
,
)
)
;
}
}
public
function
primeSVNInfoResult
(
$path
,
$result
)
{
$this
->
svnInfoRaw
[
$path
]
=
$result
;
return
$this
;
}
public
function
primeSVNDiffResult
(
$path
,
$result
)
{
$this
->
svnDiffRaw
[
$path
]
=
$result
;
return
$this
;
}
public
function
getSVNInfo
(
$path
)
{
if
(
empty
(
$this
->
svnInfo
[
$path
]
)
)
{
if
(
empty
(
$this
->
svnInfoRaw
[
$path
]
)
)
{
$this
->
svnInfoRaw
[
$path
]
=
$this
->
buildInfoFuture
(
$path
)
->
resolve
(
)
;
}
list
(
$err
,
$stdout
)
=
$this
->
svnInfoRaw
[
$path
]
;
if
(
$err
)
{
throw
new
Exception
(
"Error #{$err} executing svn info against '{$path}'."
)
;
}
// TODO: Hack for Windows.
$stdout
=
str_replace
(
"\r\n"
,
"\n"
,
$stdout
)
;
$patterns
=
array
(
'/^(URL): (\S+)$/m'
,
'/^(Revision): (\d+)$/m'
,
'/^(Last Changed Author): (\S+)$/m'
,
'/^(Last Changed Rev): (\d+)$/m'
,
'/^(Last Changed Date): (.+) \(.+\)$/m'
,
'/^(Copied From URL): (\S+)$/m'
,
'/^(Copied From Rev): (\d+)$/m'
,
'/^(Repository UUID): (\S+)$/m'
,
'/^(Node Kind): (\S+)$/m'
,
)
;
$result
=
array
(
)
;
foreach
(
$patterns
as
$pattern
)
{
$matches
=
null
;
if
(
preg_match
(
$pattern
,
$stdout
,
$matches
)
)
{
$result
[
$matches
[
1
]
]
=
$matches
[
2
]
;
}
}
if
(
isset
(
$result
[
'Last Changed Date'
]
)
)
{
$result
[
'Last Changed Date'
]
=
strtotime
(
$result
[
'Last Changed Date'
]
)
;
}
if
(
empty
(
$result
)
)
{
throw
new
Exception
(
'Unable to parse SVN info.'
)
;
}
$this
->
svnInfo
[
$path
]
=
$result
;
}
return
$this
->
svnInfo
[
$path
]
;
}
public
function
getRawDiffText
(
$path
)
{
$status
=
$this
->
getSVNStatus
(
)
;
if
(
!
isset
(
$status
[
$path
]
)
)
{
return
null
;
}
$status
=
$status
[
$path
]
;
// Build meaningful diff text for "svn copy" operations.
if
(
$status
&
ArcanistRepositoryAPI
::
FLAG_ADDED
)
{
$info
=
$this
->
getSVNInfo
(
$path
)
;
if
(
!
empty
(
$info
[
'Copied From URL'
]
)
)
{
return
$this
->
buildSyntheticAdditionDiff
(
$path
,
$info
[
'Copied From URL'
]
,
$info
[
'Copied From Rev'
]
)
;
}
}
// If we run "diff" on a binary file which doesn't have the "svn:mime-type"
// of "application/octet-stream", `diff' will explode in a rain of
// unhelpful hellfire as it tries to build a textual diff of the two
// files. We just fix this inline since it's pretty unambiguous.
// TODO: Move this to configuration?
$matches
=
null
;
if
(
preg_match
(
'/\.(gif|png|jpe?g|swf|pdf|ico)$/i'
,
$path
,
$matches
)
)
{
$mime
=
$this
->
getSVNProperty
(
$path
,
'svn:mime-type'
)
;
if
(
$mime
!=
'application/octet-stream'
)
{
execx
(
'svn propset svn:mime-type application/octet-stream %s'
,
$this
->
getPath
(
$path
)
)
;
}
}
if
(
empty
(
$this
->
svnDiffRaw
[
$path
]
)
)
{
$this
->
svnDiffRaw
[
$path
]
=
$this
->
buildDiffFuture
(
$path
)
->
resolve
(
)
;
}
list
(
$err
,
$stdout
,
$stderr
)
=
$this
->
svnDiffRaw
[
$path
]
;
// Note: GNU Diff returns 2 when SVN hands it binary files to diff and they
// differ. This is not an error; it is documented behavior. But SVN isn't
// happy about it. SVN will exit with code 1 and return the string below.
if
(
$err
!=
0
&&
$stderr
!==
"svn: 'diff' returned 2\n"
)
{
throw
new
Exception
(
"svn diff returned unexpected error code: $err\n"
.
"stdout: $stdout\n"
.
"stderr: $stderr"
)
;
}
if
(
$err
==
0
&&
empty
(
$stdout
)
)
{
// If there are no changes, 'diff' exits with no output, but that means
// we can not distinguish between empty and unmodified files. Build a
// synthetic "diff" without any changes in it.
return
$this
->
buildSyntheticUnchangedDiff
(
$path
)
;
}
return
$stdout
;
}
protected
function
buildSyntheticAdditionDiff
(
$path
,
$source
,
$rev
)
{
$type
=
$this
->
getSVNProperty
(
$path
,
'svn:mime-type'
)
;
if
(
$type
==
'application/octet-stream'
)
{
return
<<<EODIFF
Index: {$path}
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
EODIFF
;
}
if
(
is_dir
(
$this
->
getPath
(
$path
)
)
)
{
return
null
;
}
$data
=
Filesystem
::
readFile
(
$this
->
getPath
(
$path
)
)
;
list
(
$orig
)
=
execx
(
'svn cat %s@%s'
,
$source
,
$rev
)
;
$src
=
new
TempFile
(
)
;
$dst
=
new
TempFile
(
)
;
Filesystem
::
writeFile
(
$src
,
$orig
)
;
Filesystem
::
writeFile
(
$dst
,
$data
)
;
list
(
$err
,
$diff
)
=
exec_manual
(
'diff -L a/%s -L b/%s -U%d %s %s'
,
str_replace
(
$this
->
getSourceControlPath
(
)
.
'/'
,
''
,
$source
)
,
$path
,
$this
->
getDiffLinesOfContext
(
)
,
$src
,
$dst
)
;
if
(
$err
==
1
)
{
// 1 means there are differences.
return
<<<EODIFF
Index: {$path}
===================================================================
{$diff}
EODIFF
;
}
else
{
return
$this
->
buildSyntheticUnchangedDiff
(
$path
)
;
}
}
protected
function
buildSyntheticUnchangedDiff
(
$path
)
{
$full_path
=
$this
->
getPath
(
$path
)
;
if
(
is_dir
(
$full_path
)
)
{
return
null
;
}
if
(
!
file_exists
(
$full_path
)
)
{
return
null
;
}
$data
=
Filesystem
::
readFile
(
$full_path
)
;
$lines
=
explode
(
"\n"
,
$data
)
;
$len
=
count
(
$lines
)
;
foreach
(
$lines
as
$key
=>
$line
)
{
$lines
[
$key
]
=
' '
.
$line
;
}
$lines
=
implode
(
"\n"
,
$lines
)
;
return
<<<EODIFF
Index: {$path}
===================================================================
--- {$path} (synthetic)
+++ {$path} (synthetic)
@@ -1,{$len} +1,{$len} @@
{$lines}
EODIFF
;
}
public
function
getAllFiles
(
)
{
// TODO: Handle paths with newlines.
$future
=
$this
->
buildLocalFuture
(
array
(
'list -R'
)
)
;
return
new
PhutilCallbackFilterIterator
(
new
LinesOfALargeExecFuture
(
$future
)
,
array
(
$this
,
'filterFiles'
)
)
;
}
public
function
getChangedFiles
(
$since_commit
)
{
// TODO: Handle paths with newlines.
list
(
$stdout
)
=
$this
->
execxLocal
(
'--xml diff --revision %s:HEAD --summarize'
,
$since_commit
)
;
$xml
=
new
SimpleXMLElement
(
$stdout
)
;
$return
=
array
(
)
;
foreach
(
$xml
->
paths
[
0
]
->
path
as
$path
)
{
$return
[
(string)
$path
]
=
$this
->
parseSVNStatus
(
$path
[
'item'
]
)
;
}
return
$return
;
}
public
function
filterFiles
(
$path
)
{
// NOTE: SVN uses '/' also on Windows.
if
(
$path
==
''
||
substr
(
$path
,
-
1
)
==
'/'
)
{
return
null
;
}
return
$path
;
}
public
function
getBlame
(
$path
)
{
$blame
=
array
(
)
;
list
(
$stdout
)
=
$this
->
execxLocal
(
'blame %s'
,
$path
)
;
$stdout
=
trim
(
$stdout
)
;
if
(
!
strlen
(
$stdout
)
)
{
// Empty file.
return
$blame
;
}
foreach
(
explode
(
"\n"
,
$stdout
)
as
$line
)
{
$m
=
array
(
)
;
if
(
!
preg_match
(
'/^\s*(\d+)\s+(\S+)/'
,
$line
,
$m
)
)
{
throw
new
Exception
(
"Bad blame? `{$line}'"
)
;
}
$revision
=
$m
[
1
]
;
$author
=
$m
[
2
]
;
$blame
[
]
=
array
(
$author
,
$revision
)
;
}
return
$blame
;
}
public
function
getOriginalFileData
(
$path
)
{
// SVN issues warnings for nonexistent paths, directories, etc., but still
// returns no error code. However, for new paths in the working copy it
// fails. Assume that failure means the original file does not exist.
list
(
$err
,
$stdout
)
=
$this
->
execManualLocal
(
'cat %s@'
,
$path
)
;
if
(
$err
)
{
return
null
;
}
return
$stdout
;
}
public
function
getCurrentFileData
(
$path
)
{
$full_path
=
$this
->
getPath
(
$path
)
;
if
(
Filesystem
::
pathExists
(
$full_path
)
)
{
return
Filesystem
::
readFile
(
$full_path
)
;
}
return
null
;
}
public
function
getRepositorySVNUUID
(
)
{
$info
=
$this
->
getSVNInfo
(
'/'
)
;
return
$info
[
'Repository UUID'
]
;
}
public
function
getLocalCommitInformation
(
)
{
return
null
;
}
public
function
isHistoryDefaultImmutable
(
)
{
return
true
;
}
public
function
supportsAmend
(
)
{
return
false
;
}
public
function
supportsRelativeLocalCommits
(
)
{
return
false
;
}
public
function
hasLocalCommit
(
$commit
)
{
return
false
;
}
public
function
getWorkingCopyRevision
(
)
{
return
$this
->
getSourceControlBaseRevision
(
)
;
}
public
function
supportsLocalBranchMerge
(
)
{
return
false
;
}
public
function
getFinalizedRevisionMessage
(
)
{
// In other VCSes we give push instructions here, but it never makes sense
// in SVN.
return
"Done."
;
}
public
function
loadWorkingCopyDifferentialRevisions
(
ConduitClient
$conduit
,
array
$query
)
{
// We don't have much to go on in SVN, look for revisions that came from
// this directory and belong to the same project.
$project
=
$this
->
getWorkingCopyIdentity
(
)
->
getProjectID
(
)
;
if
(
!
$project
)
{
return
array
(
)
;
}
$results
=
$conduit
->
callMethodSynchronous
(
'differential.query'
,
$query
+
array
(
'arcanistProjects'
=>
array
(
$project
)
,
)
)
;
foreach
(
$results
as
$key
=>
$result
)
{
if
(
$result
[
'sourcePath'
]
!=
$this
->
getPath
(
)
)
{
unset
(
$results
[
$key
]
)
;
}
}
foreach
(
$results
as
$key
=>
$result
)
{
$results
[
$key
]
[
'why'
]
=
"Matching arcanist project name and working copy directory path."
;
}
return
$results
;
}
public
function
updateWorkingCopy
(
)
{
$this
->
execxLocal
(
'up'
)
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Jan 19, 17:17 (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126878
Default Alt Text
ArcanistSubversionAPI.php (16 KB)
Attached To
Mode
rARC Arcanist
Attached
Detach File
Event Timeline
Log In to Comment