Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F92690551
DiffusionRequest.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Fri, Nov 22, 19:41
Size
18 KB
Mime Type
text/x-php
Expires
Sun, Nov 24, 19:41 (1 d, 18 h)
Engine
blob
Format
Raw Data
Handle
22476559
Attached To
rPH Phabricator
DiffusionRequest.php
View Options
<?php
/**
* Contains logic to parse Diffusion requests, which have a complicated URI
* structure.
*
* @task new Creating Requests
* @task uri Managing Diffusion URIs
*/
abstract
class
DiffusionRequest
extends
Phobject
{
protected
$path
;
protected
$line
;
protected
$branch
;
protected
$lint
;
protected
$symbolicCommit
;
protected
$symbolicType
;
protected
$stableCommit
;
protected
$repository
;
protected
$repositoryCommit
;
protected
$repositoryCommitData
;
private
$isClusterRequest
=
false
;
private
$initFromConduit
=
true
;
private
$user
;
private
$branchObject
=
false
;
private
$refAlternatives
;
abstract
public
function
supportsBranches
();
abstract
protected
function
isStableCommit
(
$symbol
);
protected
function
didInitialize
()
{
return
null
;
}
/* -( Creating Requests )-------------------------------------------------- */
/**
* Create a new synthetic request from a parameter dictionary. If you need
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
* can use this method to build one.
*
* Parameters are:
*
* - `repository` Repository object or identifier.
* - `user` Viewing user. Required if `repository` is an identifier.
* - `branch` Optional, branch name.
* - `path` Optional, file path.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
*
* @param map See documentation.
* @return DiffusionRequest New request object.
* @task new
*/
final
public
static
function
newFromDictionary
(
array
$data
)
{
$repository_key
=
'repository'
;
$identifier_key
=
'callsign'
;
$viewer_key
=
'user'
;
$repository
=
idx
(
$data
,
$repository_key
);
$identifier
=
idx
(
$data
,
$identifier_key
);
$have_repository
=
(
$repository
!==
null
);
$have_identifier
=
(
$identifier
!==
null
);
if
(
$have_repository
&&
$have_identifier
)
{
throw
new
Exception
(
pht
(
'Specify "%s" or "%s", but not both.'
,
$repository_key
,
$identifier_key
));
}
if
(!
$have_repository
&&
!
$have_identifier
)
{
throw
new
Exception
(
pht
(
'One of "%s" and "%s" is required.'
,
$repository_key
,
$identifier_key
));
}
if
(
$have_repository
)
{
if
(!(
$repository
instanceof
PhabricatorRepository
))
{
if
(
empty
(
$data
[
$viewer_key
]))
{
throw
new
Exception
(
pht
(
'Parameter "%s" is required if "%s" is provided.'
,
$viewer_key
,
$identifier_key
));
}
$identifier
=
$repository
;
$repository
=
null
;
}
}
if
(
$identifier
!==
null
)
{
$object
=
self
::
newFromIdentifier
(
$identifier
,
$data
[
$viewer_key
]);
}
else
{
$object
=
self
::
newFromRepository
(
$repository
);
}
if
(!
$object
)
{
return
null
;
}
$object
->
initializeFromDictionary
(
$data
);
return
$object
;
}
/**
* Create a new request from an Aphront request dictionary. This is an
* internal method that you generally should not call directly; instead,
* call @{method:newFromDictionary}.
*
* @param map Map of Aphront request data.
* @return DiffusionRequest New request object.
* @task new
*/
final
public
static
function
newFromAphrontRequestDictionary
(
array
$data
,
AphrontRequest
$request
)
{
$identifier
=
phutil_unescape_uri_path_component
(
idx
(
$data
,
'callsign'
));
$object
=
self
::
newFromIdentifier
(
$identifier
,
$request
->
getUser
());
$use_branches
=
$object
->
supportsBranches
();
if
(
isset
(
$data
[
'dblob'
]))
{
$parsed
=
self
::
parseRequestBlob
(
idx
(
$data
,
'dblob'
),
$use_branches
);
}
else
{
$parsed
=
array
(
'commit'
=>
idx
(
$data
,
'commit'
),
'path'
=>
idx
(
$data
,
'path'
),
'line'
=>
idx
(
$data
,
'line'
),
'branch'
=>
idx
(
$data
,
'branch'
),
);
}
$object
->
setUser
(
$request
->
getUser
());
$object
->
initializeFromDictionary
(
$parsed
);
$object
->
lint
=
$request
->
getStr
(
'lint'
);
return
$object
;
}
/**
* Internal.
*
* @task new
*/
final
private
function
__construct
()
{
// <private>
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param string Repository identifier.
* @param PhabricatorUser Viewing user.
* @return DiffusionRequest New request object.
* @task new
*/
final
private
static
function
newFromIdentifier
(
$identifier
,
PhabricatorUser
$viewer
)
{
$repository
=
id
(
new
PhabricatorRepositoryQuery
())
->
setViewer
(
$viewer
)
->
withIdentifiers
(
array
(
$identifier
))
->
executeOne
();
if
(!
$repository
)
{
return
null
;
}
return
self
::
newFromRepository
(
$repository
);
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param PhabricatorRepository Repository object.
* @return DiffusionRequest New request object.
* @task new
*/
final
private
static
function
newFromRepository
(
PhabricatorRepository
$repository
)
{
$map
=
array
(
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
=>
'DiffusionGitRequest'
,
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
=>
'DiffusionSvnRequest'
,
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
=>
'DiffusionMercurialRequest'
,
);
$class
=
idx
(
$map
,
$repository
->
getVersionControlSystem
());
if
(!
$class
)
{
throw
new
Exception
(
pht
(
'Unknown version control system!'
));
}
$object
=
new
$class
();
$object
->
repository
=
$repository
;
return
$object
;
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param map Map of parsed data.
* @return void
* @task new
*/
final
private
function
initializeFromDictionary
(
array
$data
)
{
$this
->
path
=
idx
(
$data
,
'path'
);
$this
->
line
=
idx
(
$data
,
'line'
);
$this
->
initFromConduit
=
idx
(
$data
,
'initFromConduit'
,
true
);
$this
->
symbolicCommit
=
idx
(
$data
,
'commit'
);
if
(
$this
->
supportsBranches
())
{
$this
->
branch
=
idx
(
$data
,
'branch'
);
}
if
(!
$this
->
getUser
())
{
$user
=
idx
(
$data
,
'user'
);
if
(!
$user
)
{
throw
new
Exception
(
pht
(
'You must provide a %s in the dictionary!'
,
'PhabricatorUser'
));
}
$this
->
setUser
(
$user
);
}
$this
->
didInitialize
();
}
final
public
function
setUser
(
PhabricatorUser
$user
)
{
$this
->
user
=
$user
;
return
$this
;
}
final
public
function
getUser
()
{
return
$this
->
user
;
}
public
function
getRepository
()
{
return
$this
->
repository
;
}
public
function
getCallsign
()
{
return
$this
->
getRepository
()->
getCallsign
();
}
public
function
setPath
(
$path
)
{
$this
->
path
=
$path
;
return
$this
;
}
public
function
getPath
()
{
return
$this
->
path
;
}
public
function
getLine
()
{
return
$this
->
line
;
}
public
function
getCommit
()
{
// TODO: Probably remove all of this.
if
(
$this
->
getSymbolicCommit
()
!==
null
)
{
return
$this
->
getSymbolicCommit
();
}
return
$this
->
getStableCommit
();
}
/**
* Get the symbolic commit associated with this request.
*
* A symbolic commit may be a commit hash, an abbreviated commit hash, a
* branch name, a tag name, or an expression like "HEAD^^^". The symbolic
* commit may also be absent.
*
* This method always returns the symbol present in the original request,
* in unmodified form.
*
* See also @{method:getStableCommit}.
*
* @return string|null Symbolic commit, if one was present in the request.
*/
public
function
getSymbolicCommit
()
{
return
$this
->
symbolicCommit
;
}
/**
* Modify the request to move the symbolic commit elsewhere.
*
* @param string New symbolic commit.
* @return this
*/
public
function
updateSymbolicCommit
(
$symbol
)
{
$this
->
symbolicCommit
=
$symbol
;
$this
->
symbolicType
=
null
;
$this
->
stableCommit
=
null
;
return
$this
;
}
/**
* Get the ref type (`commit` or `tag`) of the location associated with this
* request.
*
* If a symbolic commit is present in the request, this method identifies
* the type of the symbol. Otherwise, it identifies the type of symbol of
* the location the request is implicitly associated with. This will probably
* always be `commit`.
*
* @return string Symbolic commit type (`commit` or `tag`).
*/
public
function
getSymbolicType
()
{
if
(
$this
->
symbolicType
===
null
)
{
// As a side effect, this resolves the symbolic type.
$this
->
getStableCommit
();
}
return
$this
->
symbolicType
;
}
/**
* Retrieve the stable, permanent commit name identifying the repository
* location associated with this request.
*
* This returns a non-symbolic identifier for the current commit: in Git and
* Mercurial, a 40-character SHA1; in SVN, a revision number.
*
* See also @{method:getSymbolicCommit}.
*
* @return string Stable commit name, like a git hash or SVN revision. Not
* a symbolic commit reference.
*/
public
function
getStableCommit
()
{
if
(!
$this
->
stableCommit
)
{
if
(
$this
->
isStableCommit
(
$this
->
symbolicCommit
))
{
$this
->
stableCommit
=
$this
->
symbolicCommit
;
$this
->
symbolicType
=
'commit'
;
}
else
{
$this
->
queryStableCommit
();
}
}
return
$this
->
stableCommit
;
}
public
function
getBranch
()
{
return
$this
->
branch
;
}
public
function
getLint
()
{
return
$this
->
lint
;
}
protected
function
getArcanistBranch
()
{
return
$this
->
getBranch
();
}
public
function
loadBranch
()
{
// TODO: Get rid of this and do real Queries on real objects.
if
(
$this
->
branchObject
===
false
)
{
$this
->
branchObject
=
PhabricatorRepositoryBranch
::
loadBranch
(
$this
->
getRepository
()->
getID
(),
$this
->
getArcanistBranch
());
}
return
$this
->
branchObject
;
}
public
function
loadCoverage
()
{
// TODO: This should also die.
$branch
=
$this
->
loadBranch
();
if
(!
$branch
)
{
return
;
}
$path
=
$this
->
getPath
();
$path_map
=
id
(
new
DiffusionPathIDQuery
(
array
(
$path
)))->
loadPathIDs
();
$coverage_row
=
queryfx_one
(
id
(
new
PhabricatorRepository
())->
establishConnection
(
'r'
),
'SELECT * FROM %T WHERE branchID = %d AND pathID = %d
ORDER BY commitID DESC LIMIT 1'
,
'repository_coverage'
,
$branch
->
getID
(),
$path_map
[
$path
]);
if
(!
$coverage_row
)
{
return
null
;
}
return
idx
(
$coverage_row
,
'coverage'
);
}
public
function
loadCommit
()
{
if
(
empty
(
$this
->
repositoryCommit
))
{
$repository
=
$this
->
getRepository
();
$commit
=
id
(
new
DiffusionCommitQuery
())
->
setViewer
(
$this
->
getUser
())
->
withRepository
(
$repository
)
->
withIdentifiers
(
array
(
$this
->
getStableCommit
()))
->
executeOne
();
if
(
$commit
)
{
$commit
->
attachRepository
(
$repository
);
}
$this
->
repositoryCommit
=
$commit
;
}
return
$this
->
repositoryCommit
;
}
public
function
loadCommitData
()
{
if
(
empty
(
$this
->
repositoryCommitData
))
{
$commit
=
$this
->
loadCommit
();
$data
=
id
(
new
PhabricatorRepositoryCommitData
())->
loadOneWhere
(
'commitID = %d'
,
$commit
->
getID
());
if
(!
$data
)
{
$data
=
new
PhabricatorRepositoryCommitData
();
$data
->
setCommitMessage
(
pht
(
'(This commit has not been fully parsed yet.)'
));
}
$this
->
repositoryCommitData
=
$data
;
}
return
$this
->
repositoryCommitData
;
}
/* -( Managing Diffusion URIs )-------------------------------------------- */
public
function
generateURI
(
array
$params
)
{
if
(
empty
(
$params
[
'stable'
]))
{
$default_commit
=
$this
->
getSymbolicCommit
();
}
else
{
$default_commit
=
$this
->
getStableCommit
();
}
$defaults
=
array
(
'path'
=>
$this
->
getPath
(),
'branch'
=>
$this
->
getBranch
(),
'commit'
=>
$default_commit
,
'lint'
=>
idx
(
$params
,
'lint'
,
$this
->
getLint
()),
);
foreach
(
$defaults
as
$key
=>
$val
)
{
if
(!
isset
(
$params
[
$key
]))
{
// Overwrite NULL.
$params
[
$key
]
=
$val
;
}
}
return
$this
->
getRepository
()->
generateURI
(
$params
);
}
/**
* Internal. Public only for unit tests.
*
* Parse the request URI into components.
*
* @param string URI blob.
* @param bool True if this VCS supports branches.
* @return map Parsed URI.
*
* @task uri
*/
public
static
function
parseRequestBlob
(
$blob
,
$supports_branches
)
{
$result
=
array
(
'branch'
=>
null
,
'path'
=>
null
,
'commit'
=>
null
,
'line'
=>
null
,
);
$matches
=
null
;
if
(
$supports_branches
)
{
// Consume the front part of the URI, up to the first "/". This is the
// path-component encoded branch name.
if
(
preg_match
(
'@^([^/]+)/@'
,
$blob
,
$matches
))
{
$result
[
'branch'
]
=
phutil_unescape_uri_path_component
(
$matches
[
1
]);
$blob
=
substr
(
$blob
,
strlen
(
$matches
[
1
])
+
1
);
}
}
// Consume the back part of the URI, up to the first "$". Use a negative
// lookbehind to prevent matching '$$'. We double the '$' symbol when
// encoding so that files with names like "money/$100" will survive.
$pattern
=
'@(?:(?:^|[^$])(?:[$][$])*)[$]([
\d
-,]+)$@'
;
if
(
preg_match
(
$pattern
,
$blob
,
$matches
))
{
$result
[
'line'
]
=
$matches
[
1
];
$blob
=
substr
(
$blob
,
0
,
-(
strlen
(
$matches
[
1
])
+
1
));
}
// We've consumed the line number if it exists, so unescape "$" in the
// rest of the string.
$blob
=
str_replace
(
'$$'
,
'$'
,
$blob
);
// Consume the commit name, stopping on ';;'. We allow any character to
// appear in commits names, as they can sometimes be symbolic names (like
// tag names or refs).
if
(
preg_match
(
'@(?:(?:^|[^;])(?:;;)*);([^;].*)$@'
,
$blob
,
$matches
))
{
$result
[
'commit'
]
=
$matches
[
1
];
$blob
=
substr
(
$blob
,
0
,
-(
strlen
(
$matches
[
1
])
+
1
));
}
// We've consumed the commit if it exists, so unescape ";" in the rest
// of the string.
$blob
=
str_replace
(
';;'
,
';'
,
$blob
);
if
(
strlen
(
$blob
))
{
$result
[
'path'
]
=
$blob
;
}
$parts
=
explode
(
'/'
,
$result
[
'path'
]);
foreach
(
$parts
as
$part
)
{
// Prevent any hyjinx since we're ultimately shipping this to the
// filesystem under a lot of workflows.
if
(
$part
==
'..'
)
{
throw
new
Exception
(
pht
(
'Invalid path URI.'
));
}
}
return
$result
;
}
/**
* Check that the working copy of the repository is present and readable.
*
* @param string Path to the working copy.
*/
protected
function
validateWorkingCopy
(
$path
)
{
if
(!
is_readable
(
dirname
(
$path
)))
{
$this
->
raisePermissionException
();
}
if
(!
Filesystem
::
pathExists
(
$path
))
{
$this
->
raiseCloneException
();
}
}
protected
function
raisePermissionException
()
{
$host
=
php_uname
(
'n'
);
throw
new
DiffusionSetupException
(
pht
(
'The clone of this repository ("%s") on the local machine ("%s") '
.
'could not be read. Ensure that the repository is in a '
.
'location where the web server has read permissions.'
,
$this
->
getRepository
()->
getDisplayName
(),
$host
));
}
protected
function
raiseCloneException
()
{
$host
=
php_uname
(
'n'
);
throw
new
DiffusionSetupException
(
pht
(
'The working copy for this repository ("%s") has not been cloned yet '
.
'on this machine ("%s"). Make sure you havestarted the Phabricator '
.
'daemons. If this problem persists for longer than a clone should '
.
'take, check the daemon logs (in the Daemon Console) to see if there '
.
'were errors cloning the repository. Consult the "Diffusion User '
.
'Guide" in the documentation for help setting up repositories.'
,
$this
->
getRepository
()->
getDisplayName
(),
$host
));
}
private
function
queryStableCommit
()
{
$types
=
array
();
if
(
$this
->
symbolicCommit
)
{
$ref
=
$this
->
symbolicCommit
;
}
else
{
if
(
$this
->
supportsBranches
())
{
$ref
=
$this
->
getBranch
();
$types
=
array
(
PhabricatorRepositoryRefCursor
::
TYPE_BRANCH
,
);
}
else
{
$ref
=
'HEAD'
;
}
}
$results
=
$this
->
resolveRefs
(
array
(
$ref
),
$types
);
$matches
=
idx
(
$results
,
$ref
,
array
());
if
(!
$matches
)
{
$message
=
pht
(
'Ref "%s" does not exist in this repository.'
,
$ref
);
throw
id
(
new
DiffusionRefNotFoundException
(
$message
))
->
setRef
(
$ref
);
}
if
(
count
(
$matches
)
>
1
)
{
$match
=
$this
->
chooseBestRefMatch
(
$ref
,
$matches
);
}
else
{
$match
=
head
(
$matches
);
}
$this
->
stableCommit
=
$match
[
'identifier'
];
$this
->
symbolicType
=
$match
[
'type'
];
}
public
function
getRefAlternatives
()
{
// Make sure we've resolved the reference into a stable commit first.
try
{
$this
->
getStableCommit
();
}
catch
(
DiffusionRefNotFoundException
$ex
)
{
// If we have a bad reference, just return the empty set of
// alternatives.
}
return
$this
->
refAlternatives
;
}
private
function
chooseBestRefMatch
(
$ref
,
array
$results
)
{
// First, filter out less-desirable matches.
$candidates
=
array
();
foreach
(
$results
as
$result
)
{
// Exclude closed heads.
if
(
$result
[
'type'
]
==
'branch'
)
{
if
(
idx
(
$result
,
'closed'
))
{
continue
;
}
}
$candidates
[]
=
$result
;
}
// If we filtered everything, undo the filtering.
if
(!
$candidates
)
{
$candidates
=
$results
;
}
// TODO: Do a better job of selecting the best match?
$match
=
head
(
$candidates
);
// After choosing the best alternative, save all the alternatives so the
// UI can show them to the user.
if
(
count
(
$candidates
)
>
1
)
{
$this
->
refAlternatives
=
$candidates
;
}
return
$match
;
}
private
function
resolveRefs
(
array
$refs
,
array
$types
)
{
// First, try to resolve refs from fast cache sources.
$cached_query
=
id
(
new
DiffusionCachedResolveRefsQuery
())
->
setRepository
(
$this
->
getRepository
())
->
withRefs
(
$refs
);
if
(
$types
)
{
$cached_query
->
withTypes
(
$types
);
}
$cached_results
=
$cached_query
->
execute
();
// Throw away all the refs we resolved. Hopefully, we'll throw away
// everything here.
foreach
(
$refs
as
$key
=>
$ref
)
{
if
(
isset
(
$cached_results
[
$ref
]))
{
unset
(
$refs
[
$key
]);
}
}
// If we couldn't pull everything out of the cache, execute the underlying
// VCS operation.
if
(
$refs
)
{
$vcs_results
=
DiffusionQuery
::
callConduitWithDiffusionRequest
(
$this
->
getUser
(),
$this
,
'diffusion.resolverefs'
,
array
(
'types'
=>
$types
,
'refs'
=>
$refs
,
));
}
else
{
$vcs_results
=
array
();
}
return
$vcs_results
+
$cached_results
;
}
public
function
setIsClusterRequest
(
$is_cluster_request
)
{
$this
->
isClusterRequest
=
$is_cluster_request
;
return
$this
;
}
public
function
getIsClusterRequest
()
{
return
$this
->
isClusterRequest
;
}
}
Event Timeline
Log In to Comment